Skip to content

Patterns

Between the presets and the raw operators sits the vocabulary that the specification literature settled on decades ago. If you know these names, use them directly; if you do not, the presets are these with friendlier names.

A pattern returns a Formula rather than a Rule — you supply the id, description and mode yourself.

Patterns.absence(P.toolCall('purgeLogs'))
the formula it compiles to
G(!toolCall(purgeLogs))

Scoped, so the prohibition only starts after something else has happened:

Patterns.absence(P.toolCall('purgeLogs'), { scope: { after: P.toolCall('publish') } })
the formula it compiles to
G(O toolCall(publish) -> !toolCall(purgeLogs))
Patterns.precedence(P.toolResult('approve'), P.toolCall('deploy'))
the formula it compiles to
G(toolCall(deploy) -> O toolResult(approve))

The enabler comes first, matching how the pattern is stated: p precedes q. This trips people up in the opposite direction from requireBefore, which takes the guarded action first — worth checking which one you are calling.

Patterns.response(P.toolCall(), P.toolResult(), { within: 4 })
the formula it compiles to
G(toolCall -> F<=4 toolResult)

response requires a window. An unbounded “eventually” cannot be decided on a finite trace: at any point where the run has not ended, the answer is still “maybe”, and a monitor that can only ever say maybe is not telling you anything.

Note the propositions with no name. P.toolCall() matches a call to anything, which is what makes this rule say “every call is answered” rather than “every call to one particular tool is answered”.

A pattern is a composition of combinators, and adding one is a few lines in packages/core/src/patterns.ts. Two things matter: build it by calling the combinators rather than constructing the tree by hand, so construction-time validation applies; and decide whether it needs a window, because anything that says “eventually” does. Extending the engine has the whole procedure.