---
url: 'https://adk.nht.io/the-loop/tools.md'
description: >-
  Schema-owned tooling: Tool, ToolRegistry, merge collision policy, and the
  per-turn registry lifecycle.
---

# Tools

## LLM summary — Tools

* A `Tool` is constructed from `{ name, description, inputSchema, handler, artifactConstructor?, meta?, ephemeral?, trusted?, onCollision? }`. `inputSchema` is a `@nhtio/validation` object schema — the **single source of truth**: it validates `args` at call time AND produces the description for `tool.describe()`. Mismatch is impossible by construction.
* `handler(args, ctx, meta) => string | Uint8Array | Media | Media[] | Promise<...>`. The handler is private — invoke only via `tool.executor(ctx)`, which validates args (`E_INVALID_TOOL_ARGS` on bad input), fires `toolExecutionStart`/`toolExecutionEnd`, computes a stable `callId = sha256(canonical({tool, args}))` matching `ToolCall.checksum`, and wraps downstream errors as `E_TOOL_DOWNSTREAM_ERROR`. `string` / `Uint8Array` returns get wrapped in `tool.artifactConstructor?.() ?? SpooledArtifact`; `Media` / `Media[]` returns bypass `artifactConstructor` and land directly on `ToolCall.results` as the explicit-modality silo. **Pick `Media` when the provider can render the bytes natively; pick the `string`/`Uint8Array` → artifact path when the model needs to work with the content through handle tools.**
* **Tool handlers are conventionally invoked by the LLM executor, inside the iteration that proposed the call** — not re-invoked by middleware, not from `turnOutputPipeline`, and not after the loop if you want the model to see the result. The executor normally calls `tool.executor(ctx)(args)`, persists the completed `ToolCall` via `ctx.storeToolCall(...)`, then returns; iteration N+1's model call sees the result in `ctx.turnToolCalls`. Middleware *reacts to* completed tool calls (counting, repetition detection, post-hoc safety); it should not re-run them.
* Trust is content, not code-path. `trusted: true` on a `Tool` flips the trust envelope for `string` / `Uint8Array` / artifact results, but it is **not** consulted when the battery renders a `Media` result — `Media.trustTier` is the source of truth there. Same rule already governs `Retrievable.trustTier` inside a trusted tool's output: a trusted tool returning third-party content does not launder it.
* `ToolRegistry` instance API: `register(tool, overwrite?)` (throws `E_TOOL_ALREADY_REGISTERED` on clash unless `overwrite: true`), `unregister(name)`, `get(name)`, `has(name)`, `all()`, `pruneEphemeral()`, `bindContext(ctx)`. Static: `ToolRegistry.merge([a, b], { onCollision })`.
* Per-turn registry is seeded from `config.tools` and is **scoped to one turn** — runner baseline never mutated by middleware.
* Collision policy values: `'throw'` (default) | `'replace'` | `'keep'`. Used by `merge` only — `register` ignores `tool.onCollision`. On `merge`, incoming tool's `onCollision` consulted first; `'throw'` falls through to merge-level option. Throws `E_TOOL_ALREADY_REGISTERED`.
* Ephemeral tools have `ephemeral: true`. `registry.bindContext(ctx)` wires `ctx.onAck(() => registry.pruneEphemeral())` so they vanish only when the dispatch acks (not on nack).
* Canonical forge pattern: `const forged = SpooledArtifact.forgeTools(ctx); const merged = ToolRegistry.merge([ctx.tools, forged]); merged.bindContext(ctx)`. Forged artifact tools set `onCollision: 'replace'` so re-forging across subclasses is silent.
* `tool.describe()` → `{ name, description, inputSchema }` where `inputSchema` is `schema.describe()` (plain object, no validators). Battery is responsible for converting to provider wire shape.
* Common mistake: capturing middleware-local state in a handler closure. Read state via `ctx.stash.get('namespace')` / `ctx.stash.set('namespace', value)` (dot-paths supported), not closures. `ctx.stash` is a `Registry` — not a plain object, so index access (`ctx.stash[…]`) does not work.

A [`Tool`](https://adk.nht.io/api/@nhtio/adk/forge/classes/Tool) is a validated callable capability the agent can offer to the model. It pairs a name and a description the model reads with an input schema that validates the model's arguments and a handler that produces the result. The shape exists because the two contracts that have to agree about a tool — *what the model is told it accepts* and *what the handler is actually willing to run* — were drawn from the same schema, so they cannot drift.

::: danger A single source of truth is not decorative
It is what stops the model from being told one contract while the handler enforces another. Every tool-call bug that starts with "the model passed something we didn't expect" is, somewhere underneath, two definitions of the same thing that were allowed to disagree. `Tool` does not let them disagree.
:::

## What a Tool is

A [`Tool`](https://adk.nht.io/api/@nhtio/adk/forge/classes/Tool) is built out of three things the model sees (name, description, input schema) and one thing the model never sees (the handler). The handler may return `string`, `Uint8Array`, [`Media`](./primitives#media), or `Media[]` — the choice between the artifact-handle silo and the native-render silo is the most consequential decision in writing a tool.

→ Continue reading: [What a Tool is](./tools/what-a-tool-is)

::: danger Where the handler normally runs: inside the executor, on the same iteration the model proposed the call
By convention, a tool's handler is invoked by the [LLM executor](./llm-dispatch/executor-seam#invoking-tools) during the same iteration that proposed it. When the model returns a tool call on iteration N, the executor should call `tool.executor(ctx)(args)`, capture the result, persist the completed [`ToolCall`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolCall) record via `ctx.storeToolCall(...)`, and only then return. Iteration N+1's model call can then see the result in `ctx.turnToolCalls`. That round trip — model proposes → executor invokes handler → executor persists → next iteration sees result — is what makes the dispatch loop useful.

Implication: pipeline middleware reacts to completed [`ToolCall`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolCall) records (counting, repetition detection, post-hoc safety, audit logging). It does not re-run handlers. Run a handler from `turnOutputPipeline` and the model never saw the result. Run it from `dispatchOutputPipeline` after the executor already did and you double-fire the side effect.
:::

## ToolRegistry

A [`ToolRegistry`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolRegistry) is a name-keyed collection of [`Tool`](https://adk.nht.io/api/@nhtio/adk/forge/classes/Tool) instances. It owns one collision policy, one lifecycle hook, and the per-turn scoping that keeps middleware edits from leaking into the runner's baseline.

→ Continue reading: [ToolRegistry](./tools/registry#toolregistry)

## Collision policy

`register` and `merge` collide on names for different reasons and therefore take different defaults: `register` fails loud on clashes, `merge` consults the incoming tool's `onCollision`.

→ Continue reading: [Collision policy](./tools/registry#collision-policy)

## Per-turn lifecycle

The registry the runner hands a turn is built fresh from `config.tools`. Anything middleware does to it is scoped to that turn; the runner's baseline never mutates.

→ Continue reading: [Per-turn lifecycle](./tools/registry#per-turn-lifecycle)

## `bindContext` and ephemeral pruning

[`ToolRegistry.bindContext`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolRegistry#bindcontext) wires an `ack` handler that prunes `ephemeral: true` tools when the dispatch completes successfully — the canonical case is the artifact-query tools forged by [`SpooledArtifact.forgeTools`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#forgetools).

→ Continue reading: [bindContext and ephemeral pruning](./tools/bind-context-and-describe#bindcontext-and-ephemeral-pruning)

## `describe()` and provider tool definitions

[`Tool.describe`](https://adk.nht.io/api/@nhtio/adk/forge/classes/Tool#describe) is the plain-object form the executor reads when it has to render the tool into provider wire shape: name, description, and the result of `schema.describe()` on the input schema.

→ Continue reading: [`describe()` and provider tool definitions](./tools/bind-context-and-describe#describe-and-provider-tool-definitions)

## Trust on the tool, not on the battery

`trusted: true` on a [`Tool`](https://adk.nht.io/api/@nhtio/adk/forge/classes/Tool) flips the executor's render envelope for `string` / `Uint8Array` / artifact results. It does *not* propagate to [`Media`](https://adk.nht.io/api/@nhtio/adk/common/classes/Media) results — `Media.trustTier` is the source of truth there.

→ Continue reading: [Trust on the tool, not on the battery](./tools/trust-and-safety#trust-on-the-tool-not-on-the-battery)

## Safety, authorisation, and human approval

The ADK cannot make your tools safe. What it gives you is the primitive every defence attaches to — gates — and the rule that gates belong inside the handler, not in middleware downstream.

→ Continue reading: [Safety, authorisation, and human approval](./tools/trust-and-safety#safety-authorisation-and-human-approval)

## Advanced details

You can write basic tools without this section. Read it before you debug checksum mismatches, `artifactConstructor` cycles, or the first time someone asks why `Media` does not go through the artifact wrap site.

→ Continue reading: [Advanced details](./tools/advanced)
