Concepts

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: 60

Fields

FieldTypeNotes
scope'tool' | 'agent_tool'tool is project-wide; agent_tool partitions per (agent_id, tool). Most teams want the latter.
limitpositive integerThe number of prior action_executed events that triggers the effect.
window_minutes1 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 where payload.tool === <this action's tool>, scoped to the project.
  • scope: 'agent_tool' — additionally scoped to the same agent_id as 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 call transfer_funds more 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: 10

Fields

FieldTypeNotes
thresholdinteger 2 ≤ N ≤ 100Fire 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 an is_error: true tool 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_limit or loop_guard to the policy entry, same shape as the API. See Policy packs.
  • API. POST /v1/policies with rate_limit or loop_guard in the body.

What they do not do

  • They do not run on action_requested. The count is over action_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 evaluateAction call. The query is indexed ((project_id, occurred_at, payload.tool) with agent_id for 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.