Your first rule
A rule is an id, a description, and a property. The property is the part with the interesting name, and you can write it without meeting the notation at all — the presets are named for what they mean.
import { Presets } from '@rhysbalevicius/ltl-core'
export const SEC_01 = Presets.requireBefore('restartService', 'queryMetrics', { id: 'SEC-01', description: 'restartService may not run before queryMetrics has returned',})That is the whole rule. requireBefore(action, prerequisite) reads the first argument as a call and the second as a
result, because “before” means the prerequisite finished — an agent that fired off the metrics query and reached for the
restart while it was still running has not read anything.
Where the tools are in scope, hand it the tools rather than their names:
export const SEC_01 = Presets.requireBefore(restartService, queryMetrics, { id: 'SEC-01', description: 'restartService may not run before queryMetrics has returned',})Identical rule, but a typo or a later rename is now a compile error instead of a rule that quietly stops matching. Propositions is the full argument, and the cases where a string is still the right answer.
Hand it to a monitor
Section titled “Hand it to a monitor”import { createMonitor } from '@rhysbalevicius/ltl-core'
export const SPEC = { rules: [SEC_01] }const monitor = createMonitor(SPEC)A monitor is the rule set plus the state of the runs it has seen. It is pure and immutable underneath: every event folds the whole set forward and hands back the verdicts that changed. That is the property everything else on this site rests on, because it means the same monitor can watch a live run, replay a file, and answer “what would this event mean?” without committing to it.
Put it in front of the tools
Section titled “Put it in front of the tools”import { createTraceSource, guardTools } from '@rhysbalevicius/ltl-mastra'
const source = createTraceSource({ runId: 'run-1' })
const guarded = guardTools(monitor, { queryMetrics, restartService, purgeLogs }, { source })guarded holds the same tools — same ids, same descriptions, same schemas — with their execute wrapped. Give those to
your agent instead of the originals and nothing else about your code changes.
What happens now
Section titled “What happens now”Nothing, yet. A rule with no mode is in observe: the call runs, the verdict is recorded, and your run behaves exactly as
it did before. That is the right first state for a rule you have just written, and watching first
is the argument for leaving it there for a while.
When you are ready for the rule to actually stop something, you add one line:
export const SEC_01 = Presets.requireBefore('restartService', 'queryMetrics', { id: 'SEC-01', description: 'restartService may not run before queryMetrics has returned', mode: 'deny',})That is stopping a call.
The formula, if you want it
Section titled “The formula, if you want it”SEC_01 compiles to this:
G(toolCall(restartService) -> O toolResult(queryMetrics))Read as: at every event, if this is a call to restartService, then at some point already, queryMetrics returned. G
is “always” and O is “once”. You can write that form directly with the combinators and get the
identical rule — the preset is a name for the same tree, not a different mechanism.
You will never need to read it to use the library. It is here because a report prints it, and a reader who meets it should know what they are looking at.