Skip to content

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:

    from latent.guardrails import GuardrailMiddleware
    from latent.guardrails.scanners.builtin import LanguageScanner, TokenLimitScanner
    
    wrapped = GuardrailMiddleware(
        agent,
        pre_scanners=[LanguageScanner(allowed_languages=["en", "he"])],
        post_scanners=[TokenLimitScanner(max_tokens=4000)],
    )
    
  • Returned from a @guardrail factory method on a ReActAgent subclass (see Guardrails for the decorator API):

    from latent.guardrails import guardrail
    from latent.guardrails.scanners.builtin import InvisibleTextScanner
    
    class MyAgent(ReActAgent):
        @guardrail(timing="pre", outcome="active")
        def invisible_chars(self):
            return InvisibleTextScanner(mode="strip")
    

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.

uv add "latent[guardrails-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.

uv add "latent[guardrails-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 @guardrail decorator, GuardrailMiddleware, events, and observability.
  • Context Engineering — how context scanners run inside an agent.
  • ReAct Agent — the agent these scanners wrap.