Enforcing rules at run time¶
Checked before the call runs¶
At run time, AgentLTL sits between the agent and its tools. For each tool call the agent proposes, it evaluates every constraint on the trace so far plus the proposed call:
sequenceDiagram
participant A as Agent
participant G as AgentLTL
participant T as Tool
A->>G: proposed call (name, arguments)
G->>G: check constraints on trace + call
alt every constraint holds
G->>T: run it
T-->>A: result (recorded in the trace)
else a constraint is broken
G-->>A: refused: which rule, why, how to comply
end
The refusal is written for the agent: the rule's reason, what about this call broke it, and the way to comply. Most of the time the agent simply does the right thing next, as in the staging-to-prod example.
Only calls that ran count as done. A refused call is never added to the trace, so it can't satisfy a later "before".
Modes: what happens on a violation¶
Each rule has a mode, which says what happens when a call breaks it. The mode is the name
you write in AGENTLTL.yaml. The severity column is the name the Python library uses for
the same thing; if you only write rule files, ignore it.
| Mode | Severity | On violation |
|---|---|---|
block |
PERSISTENT_BLOCK |
Refused every time; the agent can't override it |
warn |
BLOCK_AND_WARN |
Refused once; the agent may repeat the exact call to override |
ask |
PERSISTENT_BLOCK, routed to the user |
The human is asked to approve; the agent can't override |
retry |
SOFT_BLOCK |
Refused a few times, then the human is asked |
stop |
HARD_STOP |
Refused, and the agent may not act until the human replies; it can still explain |
log |
TOLERATE |
Allowed; the agent is told it broke the rule |
When one call breaks several rules, the strongest mode decides, so a warn override can't
slip a call past a block rule.
The Python library exposes the severities directly, with escalation policies for
SOFT_BLOCK (cumulative, consecutive, hybrid): see
constraint severities.
Judging a run that isn't over¶
A call is checked on a partial trace: the run may still do anything afterwards. So at run time a formula takes one of five values, not two:
| Value | Meaning | Example |
|---|---|---|
| violated | broken, and nothing later can repair it | never deploy, after a deploy |
| not met yet | an obligation still open: a later call can meet it | called("pytest") before pytest runs |
| pending | not triggered, or not decided yet | before("test", "push") before any push; "eventually X" |
| holds so far | satisfied, but a later call could break it | at most 2 deploys after one |
| holds | satisfied, and nothing later can change that | called("pytest") after pytest ran |
A call is refused when the value drops below pending. Negation mirrors the scale, so
!before("a", "b") is pending (not violated) while no b has run, and "next X" at the last
call is pending, not false.
On a finished trace (scoring a run afterwards) the ordinary two-valued semantics apply.
Refusing only what a call breaks¶
Once a rule is broken for good, it stays broken: say a warn rule was overridden on
purpose. Refusing every later call for it would lock the agent out of its tools and repair
nothing. So AgentLTL tracks which instance of a rule failed (a position of G, a side of
&, an entity of a quantifier, a count) and refuses a call only when it creates a new
failure:
rule: G(now("deploy") -> X(G(!now("deploy")))) # deploy at most once, warn mode
deploy allowed
deploy refused (warning)
deploy allowed: the agent insisted
ls allowed: the broken instance is the earlier deploy's, not this call's
deploy refused: a new violation
An obligation that isn't met yet is never final, so a call can always still be refused for it, and steered towards meeting it.
Which formulas can be enforced¶
AgentLTL classifies every constraint before a run, from the values it can take:
- SAFE: it only fails for good ("never X", "at most N", "B only after A",
before(a, b)): every refusal points at a real violation made by that call. - UNSAFE: it can fail while an obligation is open (a bare
called("x")), so a blocking severity refuses every call until the obligation is met. - INERT: it can't fail before the run ends ("eventually X",
in_order), so a blocking severity would never fire. Check it when the agent finishes, or bound it: "within 3 steps after X, Y" is SAFE. - AMBIGUOUS: a predicate that declared nothing, or an unknown node.
The Python library warns when an UNSAFE or INERT constraint gets a blocking severity; the harnesses reject such rules. Details: runtime-safety classification.
Memory: which calls count¶
A harness decides which trace a rule reads. In the Claude Code, Copilot CLI, Mistral Vibe and Codex plugins:
- session (default): the calls of this session. It survives compaction and resume, and starts empty in a new session.
- project: every call made in the project, across sessions. Use it for rules such as "prod only gets what staging ran", where staging may have been yesterday.