Tool-call strategies¶
PIIAnonymizationMiddleware sits on two distinct channels, and they have very different reliability guarantees. Picking the right ToolCallStrategy starts with understanding why.
Two channels, two mechanisms¶
The LLM channel: cache-based, reliable¶
In abefore_model, the middleware sends an exact anonymised text to the LLM and stores the mapping hash(anonymized_text) → original in cache. When the LLM replies, aafter_model looks up the reply by hash and restores the original. This is a deterministic key lookup, it cannot be ambiguous, and it works regardless of whether two entities share a placeholder: the key is the full text, not the placeholders themselves.
As long as the LLM forwards back the exact anonymised string (which is the contract for inbound messages), this channel is reliable.
The tool channel: string-replacement, fragile¶
In awrap_tool_call, the LLM produces tool arguments by combining, splitting, paraphrasing the placeholders it just saw. That arbitrary text was never produced by the pipeline, so it is not in the cache. The same is true of the tool response: PIIGhost has never seen it before.
Both directions therefore fall back on plain string replacement:
- Tool args (LLM → tool): scan the args for known placeholders, replace each with the original value of its entity.
- Tool response (tool → LLM): scan the response for known PII values, replace each with the corresponding placeholder.
Plain replacement only works when the mapping is unambiguous. If two entities share the placeholder <<PERSON>>, there is no way to decide which original to restore in the args. If two entities collapse onto the same masked placeholder in the response, the conversation memory becomes lossy. This is the structural reason the middleware accepts only factories tagged PreservesIdentity. See Placeholder factories.
The three strategies¶
ToolCallStrategy is the dial that decides what crosses the tool boundary.
| Strategy | Tool sees | Response handling | When to use |
|---|---|---|---|
FULL (default) |
real values (deanonymised args) | re-detected and re-anonymised through the full pipeline | tools that may emit new PII (DBs, CRMs, search APIs) |
INBOUND_ONLY |
real values (deanonymised args) | passed through unchanged; lazily re-anonymised on the next abefore_model |
tools whose response is known PII-free or already-anonymised |
PASSTHROUGH |
placeholders verbatim | passed through unchanged | tools that must never see real PII, or that don't need them |
FULL¶
Symmetric: deanonymise args, run the response through pipeline.anonymize(), which re-detects and re-anonymises. New PII the tool returned is caught and turned into placeholders before the LLM sees it. Pay one detection pass per tool call.
INBOUND_ONLY¶
Cheaper: skip the detection pass on the response and let the next abefore_model catch any PII as ambient text. Picks up some latency from the deferred work, but avoids re-running NER on a structured tool output that is known to be clean (an internal id lookup, a status flag, a numeric count).
PASSTHROUGH¶
Strictest privacy boundary: tools never observe real PII. The tool receives the placeholder string as-is, and its response is forwarded back without rewriting. Useful when the agent's tools work on opaque identifiers, or when the tool is itself the LLM-facing layer of a separate anonymisation system.
PASSTHROUGH is the only mode that tolerates a PreservesLabel / PreservesShape / PreservesNothing factory. Since the tool boundary is never crossed in clear text, the unique-placeholder requirement disappears. (You still cannot wire such a factory into PIIAnonymizationMiddleware directly, because the type-checker rejects it; the escape hatch is to use the bare pipeline outside the middleware.)
Picking a strategy¶
flowchart TD
A{Tool reads or returns PII?} -->|tool must read PII| B{Tool may emit new PII in its response?}
A -->|tool needs no PII| E[PASSTHROUGH]
B -->|yes| C[FULL]
B -->|no| D[INBOUND_ONLY]
Rule of thumb:
- Default to
FULL. It is the most defensive setting and the only one that catches tool-introduced PII automatically. - Drop to
INBOUND_ONLYonly when you can prove the tool response shape is PII-free, and the latency saving matters. - Use
PASSTHROUGHwhen privacy outweighs functionality, or when the tool is engineered to work on placeholders.
See also¶
- Placeholder factories: the unique-placeholder constraint that drives
PreservesIdentity. - Architecture: sequence diagrams of the LLM and tool channels.
- Limitations: cache backend choice and how it interacts with the strategy.