---
url: 'https://adk.nht.io/the-loop/tools/advanced.md'
description: >-
  callId derivation, why artifactConstructor is a resolver, and why it does not
  accept a Media constructor.
---

# Advanced details

You can write simple tools without this page. Read it when a `callId` does not match, `artifactConstructor` looks like needless indirection, or someone tries to return `Media` through the artifact path. These are not trivia questions; they are where the weird bugs live.

[Tools](../tools) covers the [`Tool`](https://adk.nht.io/api/@nhtio/adk/forge/classes/Tool) constructor and handler contract.

## How the `callId` is derived

The executor hashes `sha256(canonicalStringify({ tool: tool.name, args }))` over the **raw, pre-validation arguments**. The `tool` field is explicitly the tool's *name string* (see [`Tool.name`](https://adk.nht.io/api/@nhtio/adk/forge/classes/Tool#property-name)), not the [`Tool`](https://adk.nht.io/api/@nhtio/adk/forge/classes/Tool) instance — refactoring fields on the class will not change the hash. `canonicalStringify` is the ADK's canonical JSON encoder: object keys sorted via `Array.prototype.sort`'s default (UTF-16 code-unit) comparator with recursion, arrays in declared order (order is meaningful for an array), primitives delegated to `JSON.stringify`. Two invocations with the same `tool.name` and the same arguments therefore produce the same `callId` regardless of argument key order, and the same value appears as [`ToolCall.checksum`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolCall#property-checksum) so the rest of the loop can correlate the call across the start event, the end event, the persisted record, and the model-facing nonce.

Because primitives go through `JSON.stringify`, arguments inherit its grammar — and its quirks. `BigInt` values and cyclic references throw `TypeError` and fail the call out loud, which is the correct outcome. `NaN`, `Infinity`, and `undefined` are the hash-collision gremlins: they do not throw, they degrade. `NaN` and `Infinity` serialise to `"null"`; `undefined` is dropped from objects and becomes `"null"` in arrays. Two different argument shapes can collapse to the same `callId`. Keep tool arguments inside the real JSON grammar — strings, finite numbers, booleans, `null`, arrays, plain objects — or accept garbage correlation.

## Why `artifactConstructor` is a resolver, not a direct reference

[`Tool.artifactConstructor`](https://adk.nht.io/api/@nhtio/adk/forge/classes/Tool#property-artifactconstructor) is typed as `() => SpooledArtifactConstructor` — a zero-argument closure that returns the [`SpooledArtifactConstructor`](https://adk.nht.io/api/@nhtio/adk/forge/type-aliases/SpooledArtifactConstructor) — rather than a direct constructor reference. The reason is a module-load cycle: [`Tool`](https://adk.nht.io/api/@nhtio/adk/forge/classes/Tool), [`SpooledArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact), and [`ArtifactTool`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ArtifactTool) all import each other transitively, and a direct reference would resolve to `undefined` at module-load time and crash at construction. The resolver is invoked at validation time, after the cycle has finished unwinding, by which point the constructor is fully defined. It's a small piece of plumbing that exists entirely so the three classes can know about each other without crashing at startup.

## Why `artifactConstructor` does not accept a `Media` constructor

[`Tool.artifactConstructor`](https://adk.nht.io/api/@nhtio/adk/forge/classes/Tool#property-artifactconstructor) is a *wrap-site* indirection — it tells the ADK "when you spool the `string` / `Uint8Array` my handler returned, wrap the bytes in this [`SpooledArtifact`](../artifacts) subclass." It exists because the handler returns raw bytes and the ADK has to decide what to wrap them in.

[`Media`](../primitives#media) bypasses that wrap site entirely. The handler constructs the `Media` itself — because the handler is the only place that knows the [`Media.kind`](https://adk.nht.io/api/@nhtio/adk/common/classes/Media#property-kind), [`Media.mimeType`](https://adk.nht.io/api/@nhtio/adk/common/classes/Media#property-mimetype), [`Media.filename`](https://adk.nht.io/api/@nhtio/adk/common/classes/Media#property-filename), [`Media.trustTier`](https://adk.nht.io/api/@nhtio/adk/common/classes/Media#property-trusttier), and [`Media.modalityHazard`](https://adk.nht.io/api/@nhtio/adk/common/classes/Media#property-modalityhazard) the primitive requires — and the ADK lands the instance on [`ToolCall.results`](https://adk.nht.io/api/@nhtio/adk/forge/classes/ToolCall#property-results) untouched. There is no raw-bytes step for `artifactConstructor` to plug into.

The two also point at deliberately disjoint contracts: [`SpooledArtifact`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact) wraps a [`SpoolReader`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/interfaces/SpoolReader) (line-indexed text — [`SpooledArtifact.head`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#head)/[`SpooledArtifact.tail`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#tail)/[`SpooledArtifact.grep`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#grep)/[`SpooledArtifact.asString`](https://adk.nht.io/api/@nhtio/adk/spooled_artifact/classes/SpooledArtifact#asstring)); `Media` wraps a [`MediaReader`](../artifacts) (opaque binary streaming — [`MediaReader.stream`](https://adk.nht.io/api/@nhtio/adk/common/interfaces/MediaReader#stream)/[`MediaReader.byteLength`](https://adk.nht.io/api/@nhtio/adk/common/interfaces/MediaReader#bytelength)). An `artifactConstructor` that resolved to a `Media` constructor would be a category error: the wrap site has bytes and no modality metadata; `Media` requires modality metadata and accepts a reader, not bytes. If your tool produces media, return `Media` directly — `artifactConstructor` is for the artifact path.
