get_weather("SF") — and pay full cost every time. The
@tool decorator lets Visceral find that duplicate work and, once it has
proven a stored result is still safe to reuse, serve it at the tool boundary
instead of executing the tool again.
@tool works on sync and async functions and composes with
node().
str
required
The tool’s stable identity. Non-empty.
bool
default:"False"
Declares the tool has no side effects. This is what lets Visceral hash the
call and capture its input/output, so the backend can find duplicate work.
Without it,
@tool only opens a span with static metadata — no hash, no
capture.bool
default:"False"
Declares that a previously computed result (fresh enough by the rule’s own
bar) is an acceptable substitute for a live call. Serving needs both
readonly and replay_safe — they are separate declarations, and one never
implies the other.Sequence[str] | None
An allow-list of argument names the call identity keys on. Defaults to
keying on all arguments. Pass e.g.
("customer_id",) to ignore volatile
arguments (a request id, a timestamp) so semantically identical calls share
one identity and reuse each other. Must be a non-empty sequence of non-empty
strings — a bare str is rejected.str | None
Bump it to invalidate every prior identity for this tool — for example after
you change what the tool returns.
float | None
A positive upper bound on how old a served result may be. It can only
shorten the freshness window the backend derived; it never lengthens it.
The two flags, concretely
readonly=True
Observe and deduplicate. Visceral hashes the call and captures its
input/output so the backend can see the same work happening twice. On its
own, it never substitutes a result — your tool always runs.
+ replay_safe=True
Reuse. With
apply=True and an active backend rule, a proven-fresh stored
result may stand in for a live call — and the tool is skipped.readonly alone gives you dedup discovery; readonly + replay_safe +
apply=True gives you actual reuse.
When a call is actually served
Serving only happens when all of these hold —readonly=True,
replay_safe=True, wrap(..., apply=True), and an active rule whose identity
matches the call. Then, before executing:
- The call’s identity hash is matched against your workspace’s active reuse rules (fetched and refreshed the same way as cache-layout rules).
- On a match, the stored result is fetched, decrypted with the workspace key the SDK already holds, and verified against the server’s content hash.
- If everything checks out, the stored value is returned and the tool never
runs. The trace records
visceral.tool.served_decision_id.
Verify sampling. At a server-controlled rate, a served call becomes a dual
execution: the real tool runs anyway, its output is captured, the span records
visceral.tool.verify_match, and your agent still receives the stored result.
That’s how Visceral keeps proving a reuse rule stays correct.Guardrails
- Fail-open means “run the real tool.” Every failure anywhere — no matching rule, no key, a timeout, a hash mismatch, a value that won’t serialize — falls through to executing the tool. A failure costs savings, never correctness. Exceptions raised by your tool propagate exactly as they would without the decorator.
- Writes invalidate reuse. Any
readonly=Falsedecorated call in the run records a writer, and serving refuses a stored result older than that writer. Wrapping your run in anode()scopes this correctly across async branches. - Content is redacted like everything else. No argument value is ever placed
on a
visceral.tool.*attribute — only the identity hash and declared metadata. Arguments and return values travel the same encrypted path as LLM content. See Privacy & data.
Kill switch
@tool reuse rides the apply=True opt-in, so it’s off until you turn apply on.
To disable just tool reuse while keeping prompt-cache optimizations, set
VISCERAL_TOOL_APPLY=0. VISCERAL_TOOL_SERVE_TIMEOUT_SECONDS (default 0.15)
bounds each serve fetch — on timeout, the real tool runs. See
Configuration.