---
url: 'https://adk.nht.io/the-loop/artifacts/forge-tools-in-depth.md'
description: >-
  The detailed forgeTools factory behaviour — snapshot logic, the callId enum,
  the ack lifecycle hook, and the staleness trade.
---

# `forgeTools(ctx)` in depth

[Artifacts](../artifacts) covers the overview. [Extending artifacts](./extending) covers the subclass pattern, [`ArtifactTool`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ArtifactTool), and what artifacts deliberately do not do.

[`SpooledArtifact.forgeTools`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#forgetools) is the static factory that takes an [`DispatchContext`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/DispatchContext) and returns a [`ToolRegistry`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolRegistry) of fresh [`ArtifactTool`](./extending#artifacttool) instances bound to the artifacts visible in [`DispatchContext.turnToolCalls`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/DispatchContext#property-turntoolcalls). It is the canonical way to hand the model a query surface over results it has already produced this turn. The behaviour is small and worth knowing in full because everything else in [Extending](./extending) composes out of it.

The factory walks [`DispatchContext.turnToolCalls`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/DispatchContext#property-turntoolcalls), filters to calls whose [`ToolCall.results`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolCall#property-results) are an instance of the class [`SpooledArtifact.forgeTools`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#forgetools) is being called on ([`SpooledArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact) for the base, [`SpooledJsonArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledJsonArtifact) for the JSON subclass, and so on), excludes any call flagged [`ToolCall.fromArtifactTool`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolCall#property-fromartifacttool)`=== true`, and collects the [`ToolCall.id`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolCall#property-id) of each remaining [`ToolCall`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolCall) into a list of `compatibleIds`. If the list is empty, the factory returns an empty `ToolRegistry` — there is no point shipping a tool whose `callId` enum has no values. You `merge` the result unconditionally; the empty case is a no-op.

When the list is non-empty, the factory mints one `ArtifactTool` per descriptor in the class's `toolMethods`. Each tool's `inputSchema` includes a `callId` field declared as `validator.string().valid(...compatibleIds).required()` — the model sees the explicit enum of valid choices in the tool definition the executor renders, and the validator rejects any callId outside that enum before the handler runs. Each tool is marked `ephemeral: true` (it belongs to one dispatch) and `onCollision: 'replace'` (so re-forging across subclasses on the same dispatch merges silently — overlapping base-method tools are behaviourally interchangeable). The handler resolves the artifact by finding the [`ToolCall`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolCall) whose [`ToolCall.id`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolCall#property-id) matches the `callId`, dispatches the [`ToolMethodDescriptor.method`](https://adk.nht.io/api/@nhtio/adk/common/interfaces/ToolMethodDescriptor#property-method) against it, and serialises the return value through `descriptor.serialise` or the default formatter (string as-is; string-array newline-joined; number stringified; otherwise `JSON.stringify` with two-space indent).

::: danger Snapshots go stale
The `callId` enum is frozen at `forgeTools(ctx)` call time. New tool calls produced *after* the snapshot are not in it. Carry a forged registry across executor invocations and the enum becomes a lie — iteration N+2 cannot reference calls produced in iteration N+1. Re-forge every executor invocation. The lifecycle hook below exists to make that automatic.
:::

The lifecycle hook is [`ToolRegistry.bindContext`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolRegistry#bindcontext), which calls [`DispatchContext.onAck`](https://adk.nht.io/api/@nhtio/adk/types/interfaces/DispatchContext#onack)`(() =>` [`ToolRegistry.pruneEphemeral`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolRegistry#pruneephemeral)`())` — pruning runs when the dispatch acks and only when it acks. Failed dispatches (`nack`) leave the forged tools in place so you can inspect what was forged when debugging the failure; the registry dies with the turn either way. Iteration boundaries would be the wrong scope to bind to, because an iteration is one model round-trip and a dispatch is several — pruning per iteration would drop the forged tools before the next iteration's model call could use them. Dispatch ack is the right scope and the only one the lifecycle uses.

::: warning Forgetting `bindContext` leaks tools to the model, not heap bytes
This is a *capability* leak — what grows is the set of tools the model can invoke, not RAM usage (and emphatically not the [`Memory`](../primitives#memory) primitive, which is unrelated). Ephemeral tools from previous dispatches stay registered and remain on offer to the model, the next [`SpooledArtifact.forgeTools`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#forgetools) sees a stale `callId` enum that excludes the calls it should be enumerating, and the model is offered handles that point at artifacts from a dispatch that has already finished. The bug is silent and the symptoms appear two iterations later, which is the worst possible combination of properties for a bug to have. The canonical wiring pattern lives in [Forging tools](../../assembly/byo-tools); copy it, do not paraphrase it.
:::
