The adapter contract
The engine knows nothing about Mastra. @rhysbalevicius/ltl-mastra is one adapter, and the shape to copy if you write
another. Read the event model first — an adapter’s whole job is producing those faithfully.
Four pieces
Section titled “Four pieces”A trace source. Numbers and timestamps the events of one run. Builders draft an event at the current position without
advancing; commit advances. That split is what makes refusal free.
The runId a source stamps is load-bearing: the monitor partitions its state by it, so events that belong to the same
run must carry the same id or they will be judged against separate histories. Sharing one source between a tool guard, a
workflow guard and an observer is how they agree — and it is also what keeps the sequence numbering dense across all
three.
A tool guard. Wrap the framework’s tool-execution function. Draft the candidate, peek, decide, commit only on
observe, run the body, commit the result. Serialise that critical section per monitor — two concurrent calls must not
each judge state the other changed. Keep the wrapper indistinguishable from the original: same prototype, same id, same
schemas.
A step guard, if the framework has workflows. Same shape, plus the framework’s own suspension primitive for
suspend, so a pause is as durable as any other pause. Notice when a step suspends for its own reasons — it has not
finished, and it must not announce a second start when it resumes.
An observer. Whatever the guards cannot see: the run’s ending, and the boundaries of steps no guard covers. Be careful about double-reporting — a framework commonly emits both a step’s outcome and its bookkeeping, and mapping both doubles every step, which halves the effective threshold of any rule that counts.
Keep the framework out of the logic
Section titled “Keep the framework out of the logic”@rhysbalevicius/ltl-mastra never imports Mastra’s types into its own reasoning; it is written against duck-typed shapes
throughout. Do the same. A framework version bump should not be able to reach the engine, and the way to guarantee that
is to make it structurally impossible rather than to be careful.
The two tests worth more than the rest
Section titled “The two tests worth more than the rest”Replay agreement. Record a real run, then run checkTrace over the recorded file and assert it produces the same
verdicts the live monitor produced. If a trace does not replay to what was seen live, the recording is lying — and
everything downstream of it, from a CI failure to a graph on this site, is lying too.
Sequence integrity. Assert events.map((event) => event.seq) equals [0, 1, 2, …] on every trace a test produces,
including runs where calls were refused. Holes and repeats are the failure mode this model is most sensitive to and the
easiest to introduce.