bindContext and describe()
The lifecycle hook that prunes ephemeral tools when a dispatch acks, and the plain-object form an executor reads to produce a provider-specific tool definition.
ToolRegistry, lifecycle, and collisions covers the registry surface, collision policy, and per-turn lifecycle.
bindContext and ephemeral pruning
A Tool marked ephemeral: true is owned by a single dispatch. The canonical case is the artifact-query tools that SpooledArtifact.forgeTools builds around a handle: they exist to let the model query a specific spooled artifact for the duration of one dispatch, and they need to leave the registry when the dispatch acknowledges completion.
ToolRegistry.bindContext is the mechanism. It registers an ack handler on the dispatch context that calls ToolRegistry.pruneEphemeral synchronously when the dispatch acks. The risk it protects against is specifically the ArtifactTool callId enum: when SpooledArtifact.forgeTools mints an artifact-query tool, the tool's input schema encodes the matching artifact IDs into a callId enum frozen at forgeTools(ctx) time. Once the dispatch ends, those IDs reference artifacts that are no longer reachable through the registry the way they were when the enum was minted. Forget the bindContext call and the registry's capability surface keeps growing across iterations — every spent dispatch's forged artifact-query tools stay registered with their now-stale callId enums, the model is offered handles that point at artifacts from a dispatch that has already finished, and a subsequent re-forge sees the same names already taken with the wrong enums underneath. The bug is silent and the symptoms appear two iterations later, which is the worst possible combination of properties for a bug to have.
Two facts worth getting right
bindContext fires on ack, not on nack. A failed dispatch leaves forged tools in place so you can inspect what was registered when debugging the failure — the registry dies with the turn either way. And bindContext returns an unsubscribe function: calling it before the dispatch acks cancels the pruning. That is rarely useful outside of tests, but it is the only honest way to opt out.
Forgetting bindContext leaks capability surface across iterations
What grows is the set of tools the model can call, not the heap. Ephemeral tools from previous dispatches stay registered, the next forgeTools(ctx) sees a stale callId enum, and the model gets offered handles that point at artifacts from a dispatch that has already finished. The canonical wiring pattern lives in Forging tools; copy it, do not paraphrase it.
describe() and provider tool definitions
Tool.describe returns the plain-object form of the tool: its name, its description, and the result of schema.describe() on the input schema — every annotation (type, description, note, example) preserved, every validator function stripped. It is the shape an DispatchExecutorFn reads when it has to produce the tool definition the provider actually expects on the wire: OpenAI's function-calling shape, Anthropic's tool-use shape, whatever a future provider chooses to invent. The tool itself stays provider-agnostic, the executor performs the translation, and the schema is the single artefact both sides agree on.