> ## Documentation Index
> Fetch the complete documentation index at: https://docs.visceralai.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Tool-call reuse

> Annotate tools with @tool so read-only calls can be deduplicated and, once proven, served from a stored result instead of re-executed.

Agents re-run the same read-only tools constantly — the same lookup, the same
retrieval, the same `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.

```python theme={"dark"}
from visceral import tool

@tool("get_customer", readonly=True, replay_safe=True)
def get_customer(customer_id: str) -> dict:
    return db.fetch_customer(customer_id)
```

`@tool` works on sync and async functions and composes with
[`node()`](/sdk/instrumenting#annotating-steps-with-node).

```python theme={"dark"}
def tool(
    name: str,
    *,
    readonly: bool = False,
    replay_safe: bool = False,
    vary: Sequence[str] | None = None,
    version: str | None = None,
    freshness_seconds: float | None = None,
)
```

<ParamField path="name" type="str" required>
  The tool's stable identity. Non-empty.
</ParamField>

<ParamField path="readonly" type="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.
</ParamField>

<ParamField path="replay_safe" type="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.
</ParamField>

<ParamField path="vary" type="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.
</ParamField>

<ParamField path="version" type="str | None">
  Bump it to invalidate every prior identity for this tool — for example after
  you change what the tool returns.
</ParamField>

<ParamField path="freshness_seconds" type="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.
</ParamField>

## The two flags, concretely

<CardGroup cols={2}>
  <Card title="readonly=True" icon="eye">
    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.
  </Card>

  <Card title="+ replay_safe=True" icon="rotate">
    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.
  </Card>
</CardGroup>

`readonly` alone gives you dedup discovery; `readonly` + `replay_safe` +
[`apply=True`](/sdk/optimization) 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:

1. The call's identity hash is matched against your workspace's active reuse
   rules (fetched and refreshed the same way as [cache-layout
   rules](/sdk/optimization#how-rules-reach-the-sdk)).
2. 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.
3. If everything checks out, the stored value is returned and **the tool never
   runs**. The trace records `visceral.tool.served_decision_id`.

<Note>
  **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.
</Note>

## 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=False` decorated call in the run
  records a writer, and serving refuses a stored result older than that writer.
  Wrapping your run in a [`node()`](/sdk/instrumenting#annotating-steps-with-node)
  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](/concepts/privacy).

## 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](/sdk/configuration).
