Developer guide

Agent guard

Check your AI agent’s tool calls, their results and its replies against rules you write in plain words.

Agent guard checks what your AI agent does against rules you write in plain words. Before the agent calls a tool, you send the call to POST /v1/guard/check. Strom answers, for each rule, the probability that the call breaks it, and tells you whether to go ahead. You can try it without code in the console’s Agent guard page.

You can check three stages of your agent’s work:

  • before_tool: a tool call, before it runs. A block means the tool doesn’t run.
  • after_tool: what a tool returned, before the agent sees it. A block means the agent doesn’t get the result. The tool has already run, so this can’t undo what it did.
  • before_reply: the agent’s reply, before the user sees it. A block means the reply isn’t sent.

Writing a rule

A rule has an id that you choose (lowercase letters, digits, - and _) and five settings:

  • text: the rule in plain words, for example “Block refunds above 500 unless the user’s role is manager.”
  • stage: before_tool (the default), after_tool or before_reply. A rule checks one stage; to check a tool both before and after it runs, write two rules.
  • tools: the tool names the rule is checked for, for example ["refund"], or ["*"] for every tool. Required for before_tool and after_tool. Leave it out for before_reply, which checks every reply.
  • mode: observe or enforce. A rule in observe mode never blocks anything. It only reports would_block, so you can see what it would catch before you enforce it.
  • threshold: how sure Strom must be that the call breaks the rule before it counts as a match, from 0 to 1. The default is 0.9.

Create and edit rules with the API:

Create a rule
curl https://api.uprelic.com/v1/guard/rules \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "refund-limit",
    "text": "Block refunds above 500 unless the user’s role is manager.",
    "stage": "before_tool",
    "tools": ["refund"],
    "mode": "enforce",
    "threshold": 0.9
  }'

The id must be new: if a rule already has it, you get a 409 and nothing changes. To change a rule, send its settings to PUT /v1/guard/rules/{id}, without the id in the body. Each change saves a new version, which checks report in rules[].version. GET /v1/guard/rules lists your rules and DELETE /v1/guard/rules/{id} deletes one. An account can have up to 256 rules.

Checking a tool call

Send the tool’s name as action and its arguments as params. Put facts from your own application, like the signed-in user’s role, in context. Take them from your own systems, not from the model’s output, since the model could make them up.

Request
curl https://api.uprelic.com/v1/guard/check \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "refund",
    "params": {"order_id": "A1", "amount": 800},
    "context": {"role": "support_agent"}
  }'
Response · illustrative
{
  "decision": "block",
  "rules": [
    {
      "id": "refund-limit", "version": 1,
      "text": "Block refunds above 500 unless the user’s role is manager.", "probability": 0.97,
      "threshold": 0.9, "mode": "enforce", "matched": true
    }
  ],
  "usage": {"rules_evaluated": 1, "input_tokens": 96, "cost": 0.000004, "currency": "usd"},
  "metadata": {"server_processing_time_ms": 92}
}

decision is one of:

  • block: a rule in enforce mode matched. Don’t run the tool.
  • would_block: only rules in observe mode matched. Run the tool.
  • allow: no rule matched, or no rule applies to this tool. Run the tool.

rules lists every rule that applied, the most likely to be broken first, with its text, its probability and whether it matched. When a call is blocked, give the agent the text of each matched rule as the tool’s result, so it can tell the user why or take another route instead of trying again.

Sessions

A session is one task of your agent, such as answering one customer email. Open one with POST /v1/guard/sessions when the task starts, and send its id as session_id with each check of that task. A check without session_id counts as a session of its own.

Request
curl -X POST https://api.uprelic.com/v1/guard/sessions \
  -H "Authorization: Bearer YOUR_API_KEY"
Response
{"id": "5f0c2a1e-8d3b-4c7a-9e21-3b6f0d9a4c11", "cost": 0.0, "currency": "usd"}

cost is the session fee: 0 while your plan's sessions for the month last. A session_id that isn't one of your sessions gets 404.

Checking a tool’s result

After the tool has run, send the same action and params with what the tool returned as result. The after_tool rules for that tool are checked. When the decision is block, don’t give the result to the agent: give it the text of each matched rule instead.

Request
curl https://api.uprelic.com/v1/guard/check \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "read_file",
    "params": {"path": "config/database.yml"},
    "result": "pool: 5\npassword: hunter2"
  }'

Checking a reply

To check a reply before the agent sends it, send response (and optionally the user’s request) instead of action. The before_reply rules are checked. When the decision is block, don’t send the reply.

Request
curl https://api.uprelic.com/v1/guard/check \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "response": "Sure, your API key is upr_k3J9xQ2mP7.",
    "request": "What is my API key?"
  }'

Using the Python SDK

Install it with pip install uprelic. Decorate each tool with client.guard(): every call is checked first, with the function’s arguments as params. With check_result=True, what the function returns is checked too.

Python
import uprelic

client = uprelic.Uprelic()  # reads UPRELIC_API_KEY

client.create_guard_rule(
    "refund-limit",
    text="Block refunds above 500 unless the user’s role is manager.",
    tools=["refund"],
    mode="enforce",
    threshold=0.9,
)

@client.guard("refund", context=lambda: {"role": current_user.role})
def refund(order_id, amount):
    ...

@client.guard("read_file", check_result=True)
def read_file(path):
    ...

try:
    refund("A1", amount=800)
except uprelic.Blocked as blocked:
    tool_result = str(blocked)  # "refund blocked by refund-limit: Block refunds above 500 …"

uprelic.Blocked is raised when the decision is block, and also when the check itself fails, so an unchecked tool never runs. Its message names the matched rules; return it to the agent as the tool’s result. To check a reply, call client.check(response=..., request=...).

Trying a rule

Every check uses the rules saved on your account. To try a new rule, save it in observe mode. It never blocks anything, so your agent keeps working while you see what the rule would catch.

Before you switch the rule to enforce, look at what it would have blocked. GET /v1/guard/checks lists your checks, newest first. Filter with decision (allow, would_block or block), and page with page and page_size (up to 100).

Request
curl "https://api.uprelic.com/v1/guard/checks?decision=would_block" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response · illustrative
{
  "checks": [
    {
      "id": "3f6c1d2e-8a4b-4c1e-9d2f-1a2b3c4d5e6f",
      "created_at": "2026-10-27T09:30:00+00:00",
      "stage": "before_tool",
      "decision": "would_block",
      "action": "refund",
      "params": {"order_id": "A1", "amount": 800},
      "context": {"role": "support_agent"},
      "rules": [{"id": "refund-limit", "version": 1, "probability": 0.97, "matched": true, ...}],
      "cost": 0.00001, "currency": "usd"
    }
  ],
  "total": 1, "page": 1, "page_size": 25
}

In Python: client.guard_checks(decision="would_block").

Stored data

What you send in a check, including tool results, is stored so you can list it, for as long as your plan keeps request contents (7 days on Free), then deleted. Checks that no rule applies to aren’t stored. Agent guard has no zero data retention option.

Cost

  • Input tokens cost what they cost on POST /v1/systemone with strom-fast: the tool call, result or reply once, plus each rule's text. usage.input_tokens is that count.
  • Each session past your plan's monthly sessions costs $0.50 per 1,000: Free includes none, Starter 50,000 and Growth 500,000. The fee is charged when you open the session, or with the check when it has no session_id.
  • A check that no rule applies to is free.
  • The cost of each check is in usage.cost.

Errors

  • 401: the API key is missing or wrong.
  • 402: your balance is too low.
  • 404: no rule has this id. 409: a rule already has this id, or the account already has 256 rules.
  • 413: the check has more than 32,000 input tokens, for example a very large tool result.
  • 422: the request is invalid, for example both action and response are sent, or a before_tool rule has no tools.
  • 429: too many requests. Checks share the rate limits of your other requests.
  • 502: the model couldn’t be reached. Treat the action as not checked.