Impraint

Rules and circuit breaker

Control risky agent actions before they run.

The circuit breaker puts your rules in front of an action. Just before your agent does something risky, such as issuing a refund, it asks Guard "may I do this?". Guard checks your organisation's rules and answers one of four things:

AnswerMeaning
AllowGo ahead
BlockRefused, with a reason your agent can show
Human approvalWait until a person approves or rejects it
ThrottleSlow down: told how many seconds to wait

You set the rules in the dashboard under Govern → Rules, or through the API. Your agent only needs the SDK.

Create a rule in the dashboard

Open Govern → Rules. The page has two tabs: Rules, which lists your rules, and Enforcement Activity, which shows what your rules have done (see Enforcement Activity). Only admins can create, change or delete rules. Other roles can see them.

Click New rule and fill in the form:

FieldWhat to enter
NameA name for the rule
Applies toAll agents, or One specific agent, then choose the agent. An agent appears in the list after it sends its first trace.
WhenWhat to measure (see the table below)
Operatorgreater than, greater or equal, less than, less or equal, equal, or not equal
ThresholdThe limit, as a number
Parameter name or Call or function nameShown only for the metrics that need it
Period (minutes)Shown for the cost and token period metrics. 1 to 10080.
ThenBlock, Throttle or Human approval
Calls allowed per minuteFor Throttle only. 1 to 100000.
Approval timeout (seconds)For Human approval only. 10 to 86400. Default 300. If nobody decides in time, the action does not run.
Message to the agentOptional. Shown to the agent as the reason.
Dry runLog matches but never block, throttle or ask for approval. Off by default.

Click Save rule. A rule that is not valid is refused, with the reason shown next to the field. An organisation can have up to 100 rules, and when you reach the limit you need to delete unused ones.

To limit every call from an agent, for example with a throttle, use the condition "Tokens in one call is greater or equal to 0". The form has a Use that condition shortcut for it.

Enforcement Activity

The Enforcement Activity tab on Govern → Rules is the history of what the circuit breaker did: every block, throttle and approval request, plus every dry-run match. The newest events are first, 50 to a page, with Previous and Next buttons. The list refreshes on its own.

Each event is one card:

  • Outcome badge: Block, Throttle or Human approval.
  • What happened, for example "Refund limit blocked support_agent's process_refund".
  • Time of the event.
  • The measurement, for example "Amount in the action was 50000 (threshold 10000)".
  • The reason the agent was given.
  • View approval, on approval events, which opens that approval.

Dry-run matches carry a DRY RUN label, are dimmed and use a dashed border. They read "would have blocked", because the agent was not stopped.

Two kinds of event are not caused by one of your rules, so they have no rule or measurement:

  • Agent paused: the agent was blocked because it is paused. The card has View agent. If the agent is still paused, an admin sees a Resume button on the card. If not, the card says "This agent is no longer paused."
  • Approval limit reached: a new approval request was blocked because too many were already waiting (10 per agent, 50 per organisation). Decide or wait for the pending ones. See Approvals.

Use the filters above the list to narrow it:

FilterChoices
Agent IDType an agent ID
OutcomeAll outcomes, Block, Throttle or Human approval
RuleAll rules, one of your rules, "Agent paused (not a rule)" or "Approval limit reached (not a rule)"
Hide dry-run matchesTick to show only real enforcement

Rows are not counts of calls

Throttle and pause events are sampled: at most one per minute per rule, however many calls were refused. The number of rows is not the number of refused calls.

Rules

A rule says: when this measurement crosses this limit, do this, for these agents.

PartValues
Metricaction_amount, single_call_tokens, single_call_cost, daily_cost, hourly_tokens, repeat_call_count, custom_field
Operatorgt, gte, lt, lte, eq, neq
ThresholdA number, not negative
Actionblock, human_approval, throttle
Scopeall_agents or specific_agent
Dry runOn: log the match but never enforce it

What each metric reads, and its label in the When list:

  • action_amount ("Amount in the action"): the number your code passes in, such as a refund amount. Most rules use this.
  • single_call_tokens ("Tokens in one call"), single_call_cost ("Cost of one call (USD)"): the size of one call.
  • daily_cost ("Cost over a period (USD)"): money the agent spent over a period. The default period is 24 hours (1440 minutes).
  • hourly_tokens ("Tokens over a period"): tokens the agent used over a period. The default period is 1 hour (60 minutes).
  • repeat_call_count ("Same call repeated"): how many times the same value has been seen, which catches loops. It needs the call or function name.
  • custom_field ("A value from the action's parameters"): a value from your own parameters. It needs the parameter name.

Bad rules are refused when you save them (HTTP 422, with the reasons).

Try a rule safely first

Turn on dry run to see what a rule would have done without enforcing it. Dry-run matches are logged, never blocked, throttled or sent for approval. You can see them in Enforcement Activity.

How a decision is made

  1. Pause first. A paused agent is blocked with rule agent_paused, whatever the rules say.
  2. All rules are checked, not just the first match. A broken rule is skipped and never breaks the others.
  3. Most restrictive wins: block, then human approval, then throttle. Equal strength: the smaller rule id wins, so the result never depends on the order rules were created.
  4. Throttle counts only when nothing stronger applied, so a blocked call spends no rate-limit quota.
  5. No match means allow.

Protect a function

The simplest way is @guard.tool. It asks Guard before the function runs and handles the answer for you:

from impraint_guard import Guard, ImpraintGuardBlocked

guard = Guard(api_key="...", agent_id="support_agent")

@guard.tool(amount="refund_amount")
def process_refund(order_id: str, refund_amount: float):
    ...  # runs only if the rules allow it

try:
    process_refund("A-100", 50000)
except ImpraintGuardBlocked as e:
    print(e.decision.rule_name, e.decision.reason)

It works on normal and async def functions, bare (@guard.tool) or with options.

Guard answersWhat the decorator does
AllowThe function runs
BlockThe function does not run. Raises ImpraintGuardBlocked, or returns an error dict with on_block="return".
ThrottleWaits the time Guard asks for and tries once more. Still over: raises ImpraintGuardThrottled.
Human approvalWaits for the person. Rejected or no answer in time: raises ImpraintGuardApprovalRejected.
Guard unreachablefail_open decides (see below)

All three exceptions share the base class ImpraintGuardDenied, which carries .decision and .as_result() (an error dict you can return to a caller).

OptionDefaultMeaning
namefunction nameThe action name your rules see
amountnoneWhich argument to send as action_amount
on_block"raise""raise" or "return"
wait_on_throttleTrueWait and retry once when rate limited
max_wait_seconds10Longest throttle wait accepted
approval_timeoutthe rule'sSeconds to wait for a person

Arguments are made safe before they are sent: text is PII-scrubbed and cut to 200 characters, arguments in scrub_fields become [REDACTED], and other objects are sent as just their type name.

To also trace the function itself, stack the two decorators:

@guard.track(action_type=ActionType.TOOL_CALL)
@guard.tool(amount="refund_amount")
def process_refund(order_id, refund_amount): ...

Inside a @guard.track function, each decision appears in the trace as one circuit_breaker step.

Ask Guard yourself

Use guard.intercept when you want to handle the answer in your own code:

decision = guard.intercept("process_refund", {"action_amount": amount})

if decision.is_blocked:
    return {"error": decision.reason}
if decision.needs_approval:
    approved = guard.wait_for_approval(decision.approval_id, decision.timeout_seconds)
    if not approved:
        return {"error": "Rejected or timed out."}
if decision.is_throttled:
    time.sleep(decision.retry_after_seconds)
# otherwise: go ahead

In an async app (FastAPI, async LangGraph), use the non-blocking versions so your event loop is never frozen:

SyncAsync
guard.intercept(...)await guard.intercept_async(...)
guard.wait_for_approval(...)await guard.wait_for_approval_async(...)
guard.shutdown()await guard.shutdown_async()

Use one Guard per event loop.

Pause an agent

Pausing is the emergency stop. Every call from that agent is blocked on its next request, whatever the rules say, and no approval request is created. It affects only that agent, and only calls that go through Guard's checks (@guard.tool or intercept). Other calls are not affected.

Only admins can pause or resume. Use the Pause or Resume button on the agent's card in the agents list, or on the agent's own page. Pausing asks you to confirm first. A paused agent stays paused until someone resumes it, and an admin can also resume it from an "Agent paused" event in Enforcement Activity.

Throttle

A throttle rule lets an agent call at most N times per minute (Calls allowed per minute in the form, throttle_rpm in the API). Over the limit, Guard does not refuse for good. It tells the agent how many seconds to wait:

decision = guard.intercept(
    "lookup", {"action_amount": 1},
    wait_on_throttle=True, max_wait_seconds=10,
)
if decision.is_throttled:  # still over the limit after waiting
    return {"error": "Rate limited, try again later."}

By default intercept only reports the throttle. With wait_on_throttle=True the SDK waits and asks once more. Each agent has its own count.

Human approval

A Human approval rule holds the call until a person approves or rejects it in the dashboard (Govern → Approvals) or Slack. Only an admin of the same organisation can decide, and the first decision is final. The agent waits 300 seconds by default, or the time you set in Approval timeout on the rule (approval_timeout_seconds in the API). If nobody answers in time, the approval becomes timed_out and the call is not allowed.

See Approvals for how to link Slack, who can decide, flood limits and what is masked.

If Guard cannot be reached

If rules cannot be loaded from the usual place, Guard falls back to its stored copy, so a budget rule is not switched off by an outage. If Guard cannot be reached at all, your fail_open setting decides: True allows the action (the default, so the agent keeps working) and False blocks it. Teams that prefer safety over uptime should set fail_open=False.

Audit log

Every rule change, API key change, login, approval decision, and pause or resume is written to a tamper-evident audit log. Each entry is chained to the one before it, so editing or deleting an entry is detectable.

Known limits in beta

  • Only checked calls are protected. A tool that forgets @guard.tool or intercept runs with no checks, and pausing the agent does not stop it.
  • Rules match on numbers only. You cannot yet write "block the tool delete_database" or "block if the text contains X".
  • A missing or non-numeric amount means action_amount is not sent, so a rule on it cannot match. A warning is logged and the call goes through unless another rule applies.
  • Throttle windows reset every minute, so an agent can make up to twice the limit across a minute boundary.
  • Budget rules can trip a few seconds late, because spend is counted after the trace is processed.
  • Waiting for a throttle in sync code blocks the thread for up to 10 seconds. Use wait_on_throttle=False to fail at once.
  • Enforcement Activity is sampled for throttle and pause events, so its row count is not a count of refused calls.
  • check_content has no async version yet.

On this page