Scanners¶
Scanners are the reusable scanning units that guardrails run. Each scanner inspects a prompt, an output, or a full message array and returns a ScanResult describing whether the content passed, an optional violation score, and any rewritten content.
Scanner vs Guardrail
A scanner only detects — it's a stateless sensor that returns a ScanResult and decides no policy. A guardrail is the policy layer that runs a scanner against a specific agent at a chosen timing (pre/post) with an outcome (active = block/retract, passive = log only). The same scanner is reusable across any agent; a guardrail binds it (or an inline rule) to one agent + timing + outcome. Scanners are the sensors; guardrails are the policy that runs them. See Guardrails for the decorator and middleware that do the binding.
Scanners are consumed two ways:
-
Programmatically, via
GuardrailMiddleware: -
Returned from a
@guardrailfactory method on aReActAgentsubclass (see Guardrails for the decorator API):
This page is the full scanner catalog and protocol reference. For the decorator, middleware, events, and observability, see Guardrails.
The scan protocol¶
A scanner is any object that exposes name: str, on_error: "ignore" | "raise", and one async scan method. Three protocols live in latent.guardrails.base:
| Protocol | Method | Runs on |
|---|---|---|
InputScanner |
async def scan(self, prompt: str) -> ScanResult |
A single input prompt (pre) |
OutputScanner |
async def scan(self, prompt: str, output: str) -> ScanResult |
An output, with its prompt (post) |
MessageInputScanner |
async def scan_messages(self, messages, tools=None) -> ScanResult |
The full message array + tool defs |
All scan methods are async def. The protocols are runtime_checkable, so duck-typed classes that match the shape qualify without explicit subclassing.
ScanResult¶
ScanResult is a TypedDict with these fields (all required when constructed directly):
| Field | Type | Description |
|---|---|---|
passed |
bool |
True if the content is acceptable |
score |
float |
Violation confidence (0.0 = clean, 1.0 = certain violation) |
rewritten_input |
str \| None |
Replacement prompt (input scanners) |
rewritten_output |
str \| None |
Replacement output (output scanners) |
rewritten_messages |
list[dict] \| None |
Replacement message array (message-level scanners / context compactors) |
blocked_response |
str \| None |
Message shown to the user when blocked |
rule_name |
str |
Name of the scanner that produced this result |
metadata |
dict |
Arbitrary scanner-specific data |
Two convenience constructors avoid spelling out every field:
from latent.guardrails.base import pass_result, error_result
pass_result("my_rule") # passing result
pass_result("my_rule", passed=False, score=1.0, reason="hit") # simple failure
error_result("my_rule", exc) # errored scanner (passes to avoid false blocks)
pass_result(name, passed=True, score=0.0, **metadata) folds keyword args into metadata.
Built-in scanners¶
General-purpose input/output scanners in latent.guardrails.scanners.
| Scanner | Type | Extra | What it does |
|---|---|---|---|
LanguageScanner(allowed_languages, min_length) |
Input | guardrails |
Blocks disallowed languages via langdetect |
InvisibleTextScanner(mode=, on_error=) |
Input | none | Detects zero-width / invisible Unicode characters |
TokenLimitScanner(max_tokens) |
Input | none | Blocks prompts over a token estimate |
LLMGuardrailScanner(config) |
Input/Output | none | LLM-backed custom rule via Judge[ViolationScore] |
InvisibleTextScanner and TokenLimitScanner need no extras. LanguageScanner needs langdetect from the guardrails extra. The ML scanners wrapping LLM Guard need guardrails-llmguard; Hebrew locale scanners need guardrails-hebrew.
uv add "latent[guardrails]" # langdetect-backed scanners (LanguageScanner)
uv add "latent[guardrails-llmguard]" # LLM Guard ML scanners
uv add "latent[guardrails-hebrew]" # Hebrew locale scanners
LanguageScanner¶
Detects the prompt language and blocks anything not in allowed_languages. Inputs shorter than min_length (after stripping) skip detection and pass. When langdetect is missing and on_error="ignore", every input passes silently; with on_error="raise" the missing dependency raises at construction.
from latent.guardrails.scanners.builtin import LanguageScanner
scanner = LanguageScanner(
allowed_languages=["en", "he"], # default
min_length=10, # default; skip detection below this length
on_error="ignore", # default
error_message=None, # default; auto-selects a localized message
)
On a block, blocked_response is error_message if given, otherwise a per-language message keyed off the detected language (falling back to English).
InvisibleTextScanner¶
Flags zero-width and invisible Unicode characters (U+200B–U+200D, U+2060, U+FEFF, soft hyphen) often used to smuggle instructions.
from latent.guardrails.scanners.builtin import InvisibleTextScanner
scanner = InvisibleTextScanner(mode="block", on_error="ignore") # both defaults
mode |
Behavior when invisible chars are found |
|---|---|
"block" (default) |
Fails the scan (passed=False, score=1.0) |
"strip" |
Passes but sets rewritten_input to the cleaned text and records stripped_count in metadata |
TokenLimitScanner¶
Blocks prompts whose estimated token count exceeds max_tokens. Estimation uses a UTF-8 byte heuristic (1 token ≈ 4 bytes), which is more accurate than character counting for multilingual text. The estimate is stored in metadata["estimated_tokens"].
from latent.guardrails.scanners.builtin import TokenLimitScanner
scanner = TokenLimitScanner(max_tokens=4000, on_error="ignore") # both defaults
LLMGuardrailScanner¶
An input/output scanner backed by Judge[ViolationScore]. The judge returns a score (0.0–1.0); scores at or above threshold are violations. The same instance works as a pre or post scanner — the prompt template receives {prompt} and, for output scanning, {output}.
from latent.guardrails.scanners.llm import (
LLMGuardrailScanner,
LLMGuardrailScannerConfig,
)
scanner = LLMGuardrailScanner(
LLMGuardrailScannerConfig(
model="gpt-4o-mini",
prompt_template="Does this text contain harmful instructions?\n\nText: {prompt}",
name="harmful_content", # default: "llm_guardrail"
threshold=0.5, # default
on_error="ignore", # default
)
)
LLMGuardrailScannerConfig requires model and prompt_template; the rest default as shown. On a judge error the scanner returns error_result (or re-raises when on_error="raise").
LLM Guard ML scanners¶
ML-backed scanners wrapping LLM Guard. They need the guardrails-llmguard extra and import from latent.guardrails.scanners.llmguard.
| Scanner | Type | Constructor | What it does |
|---|---|---|---|
PromptInjectionScanner |
Input | PromptInjectionScanner(threshold=0.95) |
Detects prompt-injection / jailbreak attempts |
ToxicityScanner |
Input | ToxicityScanner(threshold=0.5) |
Flags toxic / abusive language |
BanTopicsScanner |
Input | BanTopicsScanner(topics=["violence", "politics"], threshold=0.75) |
Blocks prompts matching banned topics |
GibberishScanner |
Input | GibberishScanner(threshold=0.7) |
Flags incoherent / gibberish text |
MaliciousURLsScanner |
Output | MaliciousURLsScanner(threshold=0.7) |
Flags malicious URLs in the response |
AnonymizeScanner |
Input | AnonymizeScanner(entity_types=["CREDIT_CARD"]) |
Redacts PII in the prompt (sets rewritten_input; never blocks) |
SensitiveScanner |
Output | SensitiveScanner(entity_types=["CREDIT_CARD"]) |
Redacts sensitive PII in the response (sets rewritten_output; never blocks) |
from latent.guardrails.scanners.llmguard import (
PromptInjectionScanner,
ToxicityScanner,
BanTopicsScanner,
)
wrapped = GuardrailMiddleware(
agent,
pre_scanners=[
PromptInjectionScanner(threshold=0.95),
ToxicityScanner(threshold=0.5),
BanTopicsScanner(topics=["violence", "politics"], threshold=0.75),
],
)
Each scanner accepts on_error="ignore" (default) or "raise". AnonymizeScanner and SensitiveScanner are passive redactors: they pass and rewrite rather than block.
Hebrew locale scanners¶
Hebrew-specific scanners need the guardrails-hebrew extra and import from latent.guardrails.locales.hebrew.
| Scanner | Type | Constructor | What it does |
|---|---|---|---|
HebrewPromptInjectionScanner |
Input | HebrewPromptInjectionScanner(threshold=0.8) |
Regex-based detector for Hebrew prompt-injection patterns |
from latent.guardrails.locales.hebrew import HebrewPromptInjectionScanner
wrapped = GuardrailMiddleware(
agent,
pre_scanners=[HebrewPromptInjectionScanner(threshold=0.8)],
)
Writing a custom scanner¶
Implement name, on_error, and an async scan. Use pass_result / error_result to avoid hand-writing every ScanResult field.
import re
from latent.guardrails.base import OnError, ScanResult, pass_result
class RegexBlocker:
name: str = "regex_blocker"
on_error: OnError = "ignore"
def __init__(self, pattern: str) -> None:
self._pattern = re.compile(pattern, re.IGNORECASE)
async def scan(self, prompt: str) -> ScanResult:
if self._pattern.search(prompt):
return ScanResult(
passed=False,
score=1.0,
rewritten_input=None,
rewritten_output=None,
rewritten_messages=None,
blocked_response="Input contains a blocked pattern.",
rule_name=self.name,
metadata={"pattern": self._pattern.pattern},
)
return pass_result(self.name)
For an output scanner, implement async def scan(self, prompt: str, output: str) -> ScanResult and set rewritten_output to rewrite (rather than block) the response. For a message-level scanner, implement async def scan_messages(self, messages, tools=None) -> ScanResult and set rewritten_messages to transform the context.
Context scanners (catalog)¶
The context-engineering scanners in latent.guardrails.scanners.context implement MessageInputScanner — they operate on the whole message array (and tool definitions) rather than a single prompt. Static validators are fast (no LLM/network calls); active compactors may return rewritten_messages; semantic detectors call an LLM and are intended for periodic/offline use. See Context Engineering for how these run inside an agent.
Static auditors (fast, read-only)¶
| Scanner | Purpose |
|---|---|
TokenBudgetAuditor |
Checks overall context utilisation against the model limit (warn/compact/critical severity). |
MiddleContentDetector |
Flags critical instructions stranded in the attention trough (middle 80% of context). |
HistoryBloatDetector |
Fails when user/assistant history exceeds a share of total tokens. |
KVCacheStabilityAuditor |
Detects cache-breaking dynamic elements (timestamps, UUIDs, session IDs) in the system prompt. |
ToolDescriptionLinter |
Validates tool definitions for description quality, parameter count, and naming. |
SystemPromptStructureAuditor |
Checks the system prompt for identity statement, edge-anchored constraints, and altitude mixing. |
Active compactors (may rewrite messages)¶
| Scanner | Purpose |
|---|---|
ObservationMaskingScanner |
Compresses consumed tool outputs, keeping the most recent intact. |
CompactionScanner |
Drops/summarises older messages when the token budget exceeds a threshold. |
ToolOutputOffloadScanner |
Offloads large tool outputs to scratch files, leaving a pointer in context. |
SummaryInjectionScanner |
Injects a periodic conversation summary into long histories. |
Semantic detectors (LLM-backed)¶
| Scanner | Purpose |
|---|---|
PoisoningDetector |
Detects hallucinated facts from tool outputs re-entering context unverified. |
DistractionScorer |
Scores how off-topic each message is relative to the task objective. |
ContradictionDetector |
Finds factual contradictions between system instructions and tool outputs. |
CompressionQualityAuditor |
Evaluates compressed context against recall/artifact/continuation/decision probes. |
See Also¶
- Guardrails — the
@guardraildecorator,GuardrailMiddleware, events, and observability. - Context Engineering — how context scanners run inside an agent.
- ReAct Agent — the agent these scanners wrap.