Guard and AsyncGuard
A Guard is the one object your app talks to. You build it once from your
policies and a decision model, then call it at each step of the agent.
Its job
A guard owns three things:
- the policies, the rules to apply;
- the model, the decision model that answers judgments (Jev by default, or any other TypeSafe model);
- the limits, how much work one check may do.
It runs a check and returns a Result. It does not call your
LLM, run your tools, or remember the conversation. You call your model
yourself, and a Redactions store carries the conversation.
The guard holds no per-conversation state. One guard can serve every user and
thread in your app.
Mental model
Think of a checkpoint with five gates, one for each place text crosses into or out of your agent:
| Gate | When to call it |
|---|---|
check_input | A user message, before your model sees it. |
check_untrusted | Retrieved text, such as a web page or a file, before it enters the prompt. |
check_tool_call | A tool call your model wants to make, before it runs. |
check_tool_result | What a tool returned, before your model reads it. |
check_output | Your model's complete reply, before the user sees it. |
Every policy declares which gates it runs at. pii runs at all of them.
injection runs on input, untrusted text, and tool calls. You pass the full
list once, and each gate picks the policies that apply.
Using it
from jes import Guard
from jes.policies import injection, pii
guard = Guard(
[pii(["EMAIL_ADDRESS"]), injection(threshold=0.8)],
model="jev-latest",
)
def handle(user_text: str) -> str:
incoming = guard.check_input(user_text)
if not incoming.ok:
return incoming.onward # a short refusal
reply = call_model(incoming.onward) # the email is a placeholder here
outgoing = guard.check_output(reply, prompt=incoming)
return outgoing.onward # the reply, email restored
Build the guard once, at startup, and keep it. Pass the input Result as
prompt= on the later checks. That way judgments see the user's request, and
the reply's placeholders are restored from the same store.
For asyncio apps, use AsyncGuard. It has the same constructor and the same
five checks, as coroutines:
from jes import AsyncGuard
async with AsyncGuard(policies, model="jev-latest") as guard:
incoming = await guard.check_input(user_text)
Good to know
- Close it when your app stops, or use
with.Guardkeeps a thread pool for its model requests.AsyncGuardholds no threads, but closes the same way. - A judgment needs a model. A guard with a judgment and no
model=raisesPolicyErrorwhen you build it. A guard of transforms alone runs offline. - Mistakes raise at startup, not mid-request. A bad threshold, two
policies with the same name, or a judgment with no model raise
PolicyErrorwhen you build the policies and the guard. - A check's model requests run in parallel. Adding judgments doesn't add
their latencies up.
deadline_s(30 seconds by default) bounds the whole check. - Backend failures raise by default. Set
on_backend_error="block"to fail closed instead. See Errors. - Tool calls block instead of redacting. A tool call that carries an email or a secret is blocked, because its arguments can't be rewritten. The model still never sees the value.
Next
- The five checks, with a retrieval-augmented turn.
- Tool calls and agents.
Limits, the budget for one check.- Reference: Guard and AsyncGuard, for every argument.