Artifacts and the Tool-Call Ledger
Every tool result from every client is one linked artifact: how a call's intent, execution and result are joined by the client tool_use_id, what each vantage point contributes, how results are scanned before they are stored, and what each dashboard column means.
On this page
A tool call is one thing that the platform sees from up to four places, and the console counts it once. This page is the contract behind the Artifacts page and the Tool calls column on every AI-activity surface.
Three entities, one key
| Entity | Table | Meaning | Who writes it |
|---|---|---|---|
| Intent | ai_request_tool_calls |
the model emitted a tool_use block |
the gateway, for every client on every wire |
| Execution | mcp_tool_executions |
the call ran, with source (where it was seen) and correlation (how surely it was joined) |
an in-process server, the proxy tap, or a client hook |
| Result | mcp_artifacts |
one typed artifact per execution; body stored once by digest in artifact_payloads |
the ingest, from any of the above |
They join on the client's tool_use_id. The tool_call_ledger view joins the
three for every reader, and names each row's state: intended (asked,
never reported as run), executed, or unattested (ran with no gateway
intent — the call bypassed the gateway).
What each client contributes
| Client | Intent (gateway) | Execution and result |
|---|---|---|
| Claude Code, Cowork | yes | every tool, via the PostToolUse hook |
| OpenCode | yes | every tool, via the tool.execute.after hook |
| Codex, Pi, other API clients | yes | MCP tools proxied through the gateway (the proxy tap); builtin tools have no reporting hook in those hosts, so they stay intended |
| In-process MCP servers | yes | always, at the server |
The Tool calls column is the intent count and is the same for every client. Executed and Artifacts need a second vantage point, which is why a Codex session can show tool calls without executions for its builtin tools — that is a fact about the host, and the page shows it rather than inferring it away.
Ingestion: the narrow waist
Every result, whichever vantage point delivers it, goes through one path:
- Normalise. The wire
CallToolResult(or a hook'stool_response, or a gateway history block) becomes a typed artifact. A body that declares a knownx-artifact-typeis stored as that type; everything else is atool_resultwith its content blocks, structured content and error flag. Image and audio bytes are never stored — only their size and digest. - Resolve the execution. By the server execution id carried in
_meta, by the execution the vantage point itself minted, or by the clienttool_use_id— all exact. A client-reported result with none of those is matched by session, tool, digest and time and marked inferred, which the console shows as a warning badge rather than hiding. - Scan before write. The installation's secret scanner runs over every string in the body and redacts each match in place; the gateway's safety scanners then run over the redacted text. Findings are recorded against the artifact. A scanner failure refuses the artifact rather than storing it unscanned.
- Store once. The body is content-addressed: a history replayed on every turn, or the same file read ten times, is one stored body with a reference count.
- Enrich, never duplicate. A call already stored from one vantage point is
enriched with the keys the next one knew (session, trace,
tool_use_id, last seen from) and its body is never replaced by a client's copy.
The proxy tap stamps the execution id into the response _meta before it
leaves the gateway, so a hook that later reports the same result carries the
exact server key.
What is kept, and how much
Nothing that reaches the platform is dropped without a digest, and every ceiling is stated so a reader knows what a missing body means.
| What | Where | Ceiling | Over the ceiling |
|---|---|---|---|
| Raw request and response bodies | ai_request_payloads.request_body, response_body |
governance.audit.payload_cap_bytes in the profile (default 1 MiB, floor 64 KiB) |
SHA-256 of the full bytes plus an 8 KiB head-and-tail excerpt |
The client's tools array |
ai_request_payloads.offered_tools |
none | — |
| The provider-transformed body | ai_request_payloads.prepared_body_sha256, prepared_tools |
none | the digest covers the whole body; the tools array it carried is kept verbatim |
| Tool result bodies | artifact_payloads by digest |
8 MiB | the artifact row keeps the digest, size and shape; no body |
A CLI result from the systemprompt MCP server |
the tool result the model receives | 1 MiB on the wire | the result is stored whole as its own artifact and the model receives a short pointer to /admin/artifacts/{id} |
The last row is what "point at artifacts" means. A systemprompt tool call
whose output is larger than a host will hand a model is not trimmed at the
server: the full result goes through the ingest above — scanned,
content-addressed, linked to the same session and trace, recorded as its own
execution whose input names the command and the execution it overflowed
from — and the tool result on the wire says how big it was, how many rows it
has, and where to read it. Trimming (a table losing trailing rows, text cut at
the bound) only happens if that store fails, and the result then says so.
Reading the pages
- Artifacts (
/admin/artifacts) lists structured results by default — the ones a server declared as data — and Show all results widens it to every tool result, builtin tools included. Filters: user, session, tool, source, skill, failed only. - Artifact detail shows the typed body, its digest and size, a provenance
strip (source, join quality, client,
tool_use_id, execution, intent request, governance decision, session, trace), every scanner finding, and a sandboxed preview for typed artifacts. - Request detail lists each tool call's state and links its artifact, shows every artifact of the session (an overflowed CLI result included), and carries a Tool schemas section: the client's tools beside the ones the gateway sent upstream, paired by name, with each rewrite flagged. On a Gemini or Vertex request the Gemini declaration rules are re-run over the client schema, so a tool shows the exact shapes the provider would have refused as sent. The hits are computed on read from the stored arrays, not recorded at request time.
- Conversations link each tool step to its stored result.
- Traces link hook tool events to the artifact they produced.
- Analysis › Skills / Conversations count artifacts per skill and per
conversation from
conversation_facts(artifact_count, split into files and cards), which theconversation_rollupjob keeps current every minute.
Where the numbers come from
A hook event keeps the tool_use_id it reported and the execution the ingest
resolved (plugin_usage_events.tool_use_id, mcp_execution_id). A governance
decision keeps the tool_use_id it governed. Skill attribution of an artifact
is the most recent skill invocation in the same session before the result was
produced. The Analysis pages aggregate the per-conversation rollup
(conversation_facts), which counts an artifact once per tool execution by
the single artifact definition in tool_activity.artifact_kind.