Skip to content

By example

Find the sentence closest to what you mean and take the line beside it. Everything here is a one-liner from the presets or the checks; the pages after this one explain how they are built.

Tools are named by string below so each line stands alone. Where the tool is in scope, pass the tool itself instead — requireBefore(restartService, queryMetrics) — and a rename becomes a compile error rather than a rule that silently stops firing. Propositions has the argument.

“It must not do X until Y has come back.”

Presets.requireBefore('restartService', 'queryMetrics')

The second argument is read as a result, not a call: an agent that fired the query and reached for the restart while it was still running has not read anything.

“Once it has done X, it must never do Y.”

Presets.neverAfter('closeIncident', 'restartService')

“X must never happen at all.”

Patterns.absence(P.toolCall('purgeLogs'))

“Every X must be answered within four events.”

Patterns.response(P.toolCall('pageOncall'), P.toolResult('pageOncall'), { within: 4 })

A window is required, because an unbounded “eventually” cannot be decided on a run that has not ended.

“A person has to approve this step.”

Presets.requireHumanBefore(P.stepStart('applyRemediation'), { mode: 'suspend' })

What counts as an approval is a run.resume — the run’s own lifecycle, which no tool call can fake.

“At most eight tool calls in a run.”

Checks.callBudget({ max: 8 })

“At most two searches.”

Checks.callBudget({ tool: 'search', max: 2 })

“Stop it repeating itself.”

Checks.loop({ n: 3 }) // the same call, same arguments, three times in a row
Checks.duplicateCall({ window: 5 }) // the same call again within five events

Repetition is keyed on the call’s arguments, which is what makes “the same call again” mean something.

“Only when the amount is over a thousand.”

Presets.requireHumanBefore(P.toolCall('refund', { amount: P.gt(1000) }), { mode: 'suspend' })

Every proposition takes an optional payload matcher. Propositions has the full vocabulary — comparison, membership, regular expressions, presence, and dotted paths into nested objects.

import { Checks, Presets } from '@rhysbalevicius/ltl-core'
import type { MonitorSpec } from '@rhysbalevicius/ltl-core'
export const SPEC: MonitorSpec = {
rules: [
Presets.requireBefore('restartService', 'queryMetrics', { id: 'SEC-01', mode: 'deny' }),
Presets.neverAfter('closeIncident', 'restartService', { id: 'LIFE-01' }),
],
checks: [Checks.callBudget({ max: 8 })],
}

Rules and checks are reported identically — same verdict shape, same modes, same place in a summary. The split exists because counting is not a temporal property, not because the two are treated differently.

Reach down a layer. Patterns is the specification literature’s vocabulary, and combinators is the operators themselves. Each layer is implemented by calling the one below it, so dropping down never means leaving the system.