Provenance and datasets¶
The provenance ledger is the mechanism that makes a Science ADK result
checkable rather than merely plausible. It is recorded by the runtime, in a
structure the agent's own code does not write, and it is what the
tool_use_verified gate reads.
What gets recorded¶
Every node's trace entry carries a provenance object with four lists:
| Key | Written by | Each entry |
|---|---|---|
tool_calls |
await self.call_tool(...) |
{"tool": "power_law_fit", "ok": true} |
inputs_read |
await self.input(...) |
{"port": "planets", "found": true} |
urls_fetched |
await self.fetch(...) |
{"url": "https://...", "ok": true} |
files_written |
self.artifact(...) |
{"file": "residuals.png"} |
"provenance": {
"tool_calls": [{"tool": "power_law_fit", "ok": true}],
"inputs_read": [{"port": "planets", "found": true}],
"urls_fetched": [],
"files_written": []
}
Failures are recorded too. If a tool raises, the entry is written with
"ok": false before the exception propagates — an agent cannot fail
invisibly and then claim the tool never ran.
Why the agent cannot forge it¶
The four recording calls are the only way to reach the outside world through the supported API, and each appends to the ledger as a side effect of doing its real work:
async def call_tool(self, name, **arguments):
try:
result = await self._services.call_tool(name, arguments)
except Exception:
self._provenance["tool_calls"].append({"tool": name, "ok": False})
raise
self._provenance["tool_calls"].append({"tool": name, "ok": True})
return result
There is no supported way to append a successful tool call without a tool having actually succeeded. The only way to make the ledger say a fit happened is to perform the fit.
The gate that reads it¶
For every node that declares tools in workflow.json and finished in state
passed or cached, the runtime requires at least one tool call with
"ok": true. Otherwise:
[FAIL] tool_use_verified: Node(s) declared tools but made no successful tool
call: analyze. Either the data was not really fetched, or the
declaration is wrong.
The error deliberately offers both readings. Sometimes the declaration is stale and the fix is to remove it; sometimes the computation was quietly replaced by a constant. The gate cannot tell the difference, so it makes a human look.
The CI fabrication test
On every push, CI runs the Kepler example, then rewrites analyze.py to
replace the call_tool line with a hard-coded perfect answer
(exponent = 1.5, r_squared = 1.0) and runs it again. Every structural
gate still passes. tool_use_verified fails and the run scores 0.00. If
that assertion ever stops holding, the build breaks.
Datasets¶
Large values would make a trace unreadable and mutable-by-accident, so they spill to disk and travel as pointers.
Any value with more than 200 elements is externalised into the run's
data/ directory:
| Value | Written as | Pointer records |
|---|---|---|
pandas.DataFrame |
<name>.parquet (falls back to .csv) |
shape, columns |
numpy.ndarray |
<name>.npy |
shape, dtype |
| Anything else JSON-able | <name>.json |
length or keys |
The trace then holds a reference rather than the data:
"outputs": {
"planets": {
"$dataset": "data/planets.parquet",
"format": "parquet",
"bytes": 184320,
"shape": [3565, 4],
"columns": ["planet_name", "period_days", "semi_major_axis_au", "host_star"]
}
}
Downstream, await self.input("planets") hydrates the pointer transparently —
the agent receives the dataframe, not the dictionary. Small values are inlined
in the trace directly, so a scalar metric stays readable.
Two consequences worth knowing:
- Traces stay diffable. A
git diffontrace.jsonshows what changed in the result, not three megabytes of re-serialised floats. - Data cannot be mutated out of band. The value a downstream node reads is the file the upstream node wrote, with its size recorded.
Reading provenance back¶
Inside an agent:
From the shell: