Skip to content
5 min read · 929 words

ToolCall

Primitives covers the eight-primitive overview.

A ToolCall is one resolved tool invocation. If a Thought is the model reasoning, a ToolCall is the model acting: it pairs the tool name and the validated arguments with whatever the handler produced, plus enough provenance for the rest of the loop to reason about what just happened. By the time the record exists the call has settled — success or error, results in hand. (The in-progress streaming shape is TurnToolCallContent, which the executor emits incrementally via helpers; the persisted record is ToolCall.)

Two of those fields do real work the moment the record is constructed. ToolCall.args is normalised — a JSON string is accepted as a convenience and the result is always a plain object — so downstream code never has to wonder which shape it has. ToolCall.checksum is computed as a stable hash of the tool name and the canonicalised arguments, so an identical call on a later iteration produces an identical checksum. That is the hook ctx.toolCallCount (see DispatchContext.toolCallCount) uses to detect the model looping on itself, and the hook an executor following nonce-keyed rendering uses to bind tool output to the call that produced it. You do not set the checksum; the constructor computes it.

ToolCall.results is the other interesting field, and it has three shapes. For a normal tool, the result is a SpooledArtifact — or SpooledArtifact[] when one call legitimately produces several bounded artifacts — because the artifact is what gives the model and the executor a uniform handle to work against regardless of payload size or shape. For an ArtifactTool call (the forged artifact_* tools that operate on an artifact), the result is a Tokenizable instead, because wrapping the answer in another SpooledArtifact would just invite the model to query the artifact it built from querying the artifact — a recursion the loop has no business entertaining. The third shape is Media — or Media[] — the explicit-modality silo for tools that return image, audio, video, or document bytes the provider can render natively. Media does not flow through Tool.artifactConstructor; it bypasses the artifact wrap the same way ArtifactTool does, because the handler has already declared the final result shape. The ToolCall.inline flag is the rendering hint that travels with the call: true (the default) tells the executor to render the artifact's content inline in the prompt; false tells it to surface the artifact as a handle and let the model fetch through the forged artifact_* tools. The producing tool, or middleware that knows better, decides which — there is no size threshold the ADK applies. See Budgets → The handle pattern for what an executor does with that hint.

ToolCall.fromArtifactTool is the marker that keeps the recursion-breaker honest: when a ToolCall came from one of the ephemeral artifact_* tools that SpooledArtifact.forgeTools forges around a handle, the flag is set, and the next round of SpooledArtifact.forgeTools filters those calls out of the callId enum it offers the model. The model cannot, for example, call artifact_grep on the result of another artifact_grep and stack handles indefinitely.

Three names, three meanings — read this once

The ADK uses three closely-related identifiers around a ToolCall, and they do not mean the same thing. The distinction matters because they appear together in error messages, observability events, and forged-tool schemas.

NameWhat it isWhere it comes fromWhat it is used for
ToolCall.idThe caller-supplied correlation key for this invocationSet by the provider/model (e.g. OpenAI's call_xyz IDs) when the request is emitted; stored verbatim on the recordCorrelating the model's request with its result; the value the forged artifact_* tools' callId enum is built from
ToolCall.checksumA content-derived hash of {tool, args}sha256(canonicalStringify({tool, args})), computed once by the ToolCall constructorDetecting model loops (DispatchContext.toolCallCount); binding tool output to the call that produced it in nonce-keyed envelopes; tamper-evidence on the record
callId (local variable)The same checksum value, in flight inside Tool.executor before the ToolCall record existsComputed by Tool.executor as sha256(canonicalStringify({tool, args})) prior to validating the args, so two semantically-identical invocations share a valueStable correlation across the toolExecutionStart / toolExecutionEnd event pair, before there is a ToolCall.id to refer to

The collision with forgeTools' "callId enum" is unfortunate but deliberate: the forged artifact_* tools accept a parameter literally called callId because that is the name the model sees in the tool schema, and callId reads more naturally to a language model than tool_call_id or correlation_key. The value the model passes is a ToolCall.id (the caller-supplied correlation key), not a checksum — the enum is built from tc.id for the calls visible on ctx.turnToolCalls. If you find yourself writing executor code that uses both senses of callId in the same function, rename one to correlationId or requestChecksum and let the prose stay short.

ToolCall.id is contractually unique, not necessarily unguessable

The contract on ToolCall.id is uniqueness within a turn — the value comes from the provider and the ADK does not regenerate it. Provider ids are opaque enough for nonce binding: OpenAI's call_* IDs are 24+ random characters. If your executor mints ToolCall.id itself (an in-process battery with no upstream provider), use crypto.randomUUID() or an equivalent unguessable id. Sequential or timestamp-based ids satisfy uniqueness and still break the trust-envelope assumption that trust-tier envelopes rely on.