---
url: 'https://adk.nht.io/the-loop/tools/registry.md'
description: >-
  The ToolRegistry surface, collision policy on register vs merge, and the
  per-turn lifecycle.
---

# ToolRegistry, lifecycle, and collisions

The registry side of the tool seam: how tools are collected, how collisions are reconciled, and how a turn's registry is scoped.

[Tools](../tools) covers the [`Tool`](https://adk.nht.io/api/@nhtio/adk/forge/classes/Tool) constructor and handler contract. [`bindContext` and `describe()`](./bind-context-and-describe) covers the ephemeral-pruning lifecycle hook and the plain-object form executors read to render provider tool definitions.

## 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 with one collision policy and one lifecycle hook. Every [`TurnRunner.run`](https://adk.nht.io/api/@nhtio/adk/turn_runner/classes/TurnRunner#run) call constructs a fresh registry from the runner's configured baseline tools, hands it to the turn via [`TurnContext`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/TurnContext), and throws it away when the turn ends. Middleware can register tools, unregister tools, merge in dynamically-built tools, and prune ephemeral tools — none of those edits touch the runner's baseline. The next turn starts with the configured tool list again, in exactly the state you wired it in with.

The instance API is intentionally small: [`ToolRegistry.register`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolRegistry#register)`(tool, overwrite?)` adds a tool (and throws `E_TOOL_ALREADY_REGISTERED` on a name clash unless `overwrite: true`); [`ToolRegistry.unregister`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolRegistry#unregister)`(name)` removes one; [`ToolRegistry.get`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolRegistry#get)`(name)` and [`ToolRegistry.has`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolRegistry#has)`(name)` are the lookups; [`ToolRegistry.all`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolRegistry#all)`()` returns a fresh array in insertion order; [`ToolRegistry.pruneEphemeral`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolRegistry#pruneephemeral)`()` drops every tool whose `ephemeral` flag is `true`; [`ToolRegistry.bindContext`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolRegistry#bindcontext)`(ctx)` is the lifecycle hook documented below.

`all()` returns a fresh array (`Array.from(values)`) of the live `Tool` references — mutating the array cannot mutate the registry, and the `Tool` instances themselves are immutable. If you want a registry with a different tool, you build a new registry or merge one in.

::: danger What the registry does not do
It does not retry. It does not rate-limit. It does not authorise. It does not cache tool definitions. It does not deduplicate calls. It does not order or prioritise. It is a name-keyed collection of validated capabilities with one lifecycle hook and one collision policy, and every behaviour that is not exactly that is a middleware concern by design.
:::

## Collision policy

Two registry operations can collide: `register` and `merge`. They take different positions on collisions because they exist for different reasons.

[`ToolRegistry.register`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolRegistry#register) is the explicit single-tool path. It throws `E_TOOL_ALREADY_REGISTERED` on a name clash unless the caller passes `overwrite: true`. The tool's own [`Tool.onCollision`](https://adk.nht.io/api/@nhtio/adk/forge/classes/Tool#property-oncollision) is ignored here — if you are calling `register` directly, the ADK assumes you already know whether you mean to replace something. Surprise replacement on a typo is the failure mode that flag is preventing.

[`ToolRegistry.merge`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolRegistry#merge) is the composition path: combine two or more registries into a fresh one without mutating any input. Baseline tools, battery tool barrels, and forged artifact-query tools meet there. Every incoming tool gets a chance to say what should happen when it collides with another. The incoming tool's [`Tool.onCollision`](https://adk.nht.io/api/@nhtio/adk/forge/classes/Tool#property-oncollision) is consulted first — `'replace'` means it wins, `'keep'` means the existing entry wins, `'throw'` (the default) means defer the decision to the merge-level option. The merge-level option, again, defaults to `'throw'`, which means a collision nobody resolved raises `E_TOOL_ALREADY_REGISTERED` and the merge fails out loud. The forged artifact-query tools set `onCollision: 'replace'` on themselves so re-forging across [`SpooledArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact) subclasses on the same dispatch resolves silently — their behaviour is interchangeable, and the only thing that would change on replacement is the closure identity nobody is reading.

::: tip Why two policies?
Because a name clash in `register` is almost always a typo or a wiring bug — fail loud, by default, no exceptions. A name clash in `merge` is almost always two intentional sources colliding on a known shared name — let the tool itself say what it wants. Same data structure, different default posture, because the contexts the two operations live in have different defaults for what surprises you want.
:::

## Per-turn lifecycle

The registry the runner hands to a turn was built fresh for that turn. Middleware reaches it through [`TurnContext.tools`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/TurnContext#property-tools) and is free to mutate it: register one-off tools, unregister tools the policy currently forbids, merge in a battery's tool barrel, merge in tools forged from a [`SpooledArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact). Whatever that turn's middleware does to its registry is local to that turn — no concurrent turn sees it, no subsequent turn inherits it, and the runner's baseline configuration is the one the next turn starts from. This is the property middleware composition relies on to be safe by default.

The one sharp edge is **ephemeral tools** — tools with [`Tool.ephemeral`](https://adk.nht.io/api/@nhtio/adk/forge/classes/Tool#property-ephemeral) set to `true` that exist only for one dispatch. Leave them registered after their dispatch and the model keeps seeing stale capabilities with dead artifact ids. That is not memory pressure; it is an authority leak. [`ToolRegistry.bindContext`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolRegistry#bindcontext) is what prevents it.

## `bindContext` and `describe()`

`registry.bindContext(ctx)` wires an `ack` handler that prunes `ephemeral: true` tools when the dispatch acks, and `tool.describe()` is the plain-object form an executor reads to produce a provider-specific tool definition.

→ Continue reading: [`bindContext` and `describe()`](./bind-context-and-describe)
