Rate limits and loop guard
Two policy clauses that fire an effect based on prior tool call history — rolling-window rate limits and per-execution loop streaks.
A regular policy condition checks the current action: tool name, an input field, a
metadata flag. rate_limit and loop_guard are two historical clauses you can
attach to a policy. They look at prior action_executed events and gate the policy's
effect on whether a count crossed a threshold.
Both are pre-filters: they run before the standard condition match in
evaluateAction, but the policy's conditions still have to match. A rate-limit policy
is always an AND with its conditions — it never fires on actions the conditions don't
target.
rate_limit and loop_guard cannot both be set on the same policy. Pick one per
policy; the schema rejects both at the boundary.
Rate limits
A rate_limit clause caps how many times a matching action can run within a sliding
window. The policy's effect (typically deny or approve) fires once the count is
reached.
- name: finance / cap-transfers-per-agent-hourly
effect: deny
conditions:
- field: tool
operator: equals
value: transfer_funds
rate_limit:
scope: agent_tool # or `tool`
limit: 5
window_minutes: 60Fields
| Field | Type | Notes |
|---|---|---|
scope | 'tool' | 'agent_tool' | tool is project-wide; agent_tool partitions per (agent_id, tool). Most teams want the latter. |
limit | positive integer | The number of prior action_executed events that triggers the effect. |
window_minutes | 1 to 7×24×60 (1 minute → 7 days) | Sliding window measured backwards from the evaluation timestamp. |
Semantics
At evaluation time Klent counts action_executed events with occurred_at >= now() - window_minutes that match the policy's scope:
scope: 'tool'— count rows wherepayload.tool === <this action's tool>, scoped to the project.scope: 'agent_tool'— additionally scoped to the sameagent_idas the current execution.
If the count is at or above limit, the policy fires. Otherwise the engine
moves on to the next policy. The payload of the resulting decision (or
action_blocked event for a deny) includes rate_limit_count and
rate_limit_window_minutes so dashboards / postmortems can show why.
When to use each scope
agent_tool— fairness between agents. "Each customer-support bot gets 5 refunds per hour, and one runaway agent can't burn the global budget."tool— a hard project-wide ceiling. "Across every agent in the project, we will not calltransfer_fundsmore than 100 times per hour, period."
Loop guard
A loop_guard clause fires the policy when the agent has called the same tool threshold
times in a row inside the current execution. It's the counter to the classic agent
failure mode: the LLM gets a result it doesn't like, retries the same tool with the same
or near-identical input, and burns minutes (and money) before someone notices.
- name: ops / loop-cap-web-search
effect: deny
conditions:
- field: tool
operator: equals
value: web_search
loop_guard:
threshold: 10Fields
| Field | Type | Notes |
|---|---|---|
threshold | integer 2 ≤ N ≤ 100 | Fire on the Nth consecutive call to this tool (the incoming). |
Semantics
The engine looks at the most recent action_executed events for the current
execution, in reverse order. It counts how many in a row targeted the same tool as the
incoming action. If that streak is threshold - 1 (so the incoming call would be the
threshold-th), the policy fires.
A different tool in between resets the streak. Multi-execution loops aren't covered — each execution is its own counter.
Why per-execution
A rate limit is "this tool has been hot project-wide". A loop guard is "this single agent run is wedged". They solve different failure modes; running both on the same deploy is normal.
Effect interactions
Both clauses still respect the rest of the policy. Combine freely:
effect: deny+rate_limit→ quota exhaustion blocks further calls.effect: approve+rate_limit→ the first N calls run unattended; calls past the limit need human approval.effect: deny+loop_guard→ break the loop and surface the policy reason back to the LLM as anis_error: truetool result (so the model picks something else).effect: approve+loop_guard→ after N consecutive same-tool calls, ask a human whether to keep going.
shadow mode also applies — set enforcement_mode: shadow to count what the policy
would do without enforcing yet, then flip to enforce once the threshold is calibrated.
Authoring
These clauses are configured on the policy, so any policy authoring surface accepts them:
- Dashboard. Open a policy in the editor; the Rate limit and Loop guard collapsibles let you set scope / limit / window or threshold inline.
- YAML. Add
rate_limitorloop_guardto the policy entry, same shape as the API. See Policy packs. - API.
POST /v1/policieswithrate_limitorloop_guardin the body.
What they do not do
- They do not run on
action_requested. The count is overaction_executed— the events that prove the tool actually ran. A loop of denied attempts does not trigger a rate limit. - They are not real-time counters. Klent re-counts on every
evaluateActioncall. The query is indexed ((project_id, occurred_at, payload.tool)withagent_idfor the per-agent variant) and stays sub-millisecond at typical project sizes. - They are not chained. Like every policy, the first match wins — see Policy engine.