IMPORTANT
enforce and mutate describe what happens to a BLOCKING violation. Since rules default to severity: 'advisory', most findings are reported through OrderingGuardResult.advisories and never reach either mode. Set severity: 'blocking' on a rule to bring it under enforce/mutate — and see the caveat on Which API Surface a Rule Applies To before you do.
Operating Modes & Repair Strategies
In production agent loops, keeping a conversation moving is often as critical as validating protocol correctness. Most validation libraries treat rejection as the only civilized response to a rule violation, even when what failed was a trivially fixable ordering quirk. Refusing to fix something you can deterministically repair isn't rigor; it's laziness that costs users a live turn.
The ordering guard battery supports two operating modes configured by the action option in OrderingGuardOptions: one that trusts nothing and refuses to touch your state, and one that actually tries to keep your execution loop alive.
TL;DR — which mode do I actually want?
enforce in development and CI. mutate in production.
enforceis your smoke detector. It will not put out the fire for you, and it will not pretend the fire isn't there. Use it while you're writing adapters, debugging a pipeline stage, or running conformance tests against synthetic traces — anywhere you want a violation to be loud, because a loud failure right now is what stops a silent one from reaching a real user later.mutateis what you actually ship. A real user mid-conversation does not care that a vendor's chat template stamped aThoughta millisecond after itsToolCall— they care that their agent kept working.mutaterepairs what's safely repairable and only escalates the violations that genuinely can't be fixed without inventing facts, so production traffic gets continuity instead of a 400 for a bug that was never theirs to cause.
Shipping enforce to production is how a mechanically-fixable ordering quirk becomes a support ticket. Shipping mutate to your test suite is how a real regression slips through disguised as a "successful" repair. The mode is an environment decision, not a personal preference — pick wrong in either direction and you've traded one failure mode for a worse one.
Quick Start: Wiring the Middleware
The validation battery exports two middleware factories from @nhtio/adk/batteries/validation:
orderingGuardDispatchMiddleware— Evaluates turn state on every dispatch iteration within the execution loop. Rejects violations viactx.nack(error).orderingGuardTurnMiddleware— Evaluates turn state once before the execution loop begins. Halts violations viactx.abort(error).
import { TurnRunner } from '@nhtio/adk'
import { orderingGuardDispatchMiddleware } from '@nhtio/adk/batteries/validation'
const runner = new TurnRunner({
executor: myExecutor,
dispatchInputPipeline: [
orderingGuardDispatchMiddleware({
// String names resolve automatically from the built-in family recipe and atomic behavior catalogs
profiles: ['anthropic-manual-thinking'],
// 'mutate' automatically fixes repairable violations; 'enforce' strictly rejects
action: 'mutate',
onViolation: 'nack', // 'nack' rejects dispatch iteration; 'throw' raises an error
onRepair: 'log', // 'log' logs warnings via console.warn; 'silent' suppresses logs
}),
],
})Profile Resolution
Passing string profile names (such as 'anthropic-manual-thinking' or 'strict_alternation') into profiles: [...] resolves them automatically from the built-in family recipe and atomic behavior registries.
The Two Modes: enforce vs mutate
export interface OrderingGuardOptions {
profiles: (string | OrderingProfile)[]
mode?: 'union-of-rules' | 'each' | 'first-match'
action?: 'enforce' | 'mutate'
onViolation?: 'nack' | 'throw'
onRepair?: 'log' | 'silent'
allowMetadataFallbackRepair?: boolean
snapshotStashKey?: string
disableAdvisoryRuleIds?: string[]
}1. action: 'enforce' (Strict Paranoid Validation)
enforce is the default mode. It performs pure, read-only validation against your configured profiles, trusting nothing, repairing nothing, and treating every blocking rule violation as an immediate dispatch failure.
If any blocking violation is detected:
- In
orderingGuardDispatchMiddleware, the iteration is rejected viactx.nack(error)(or throwsE_ORDERING_VIOLATIONifonViolation: 'throw'). - In
orderingGuardTurnMiddleware(which has nonackcapability), execution is halted viactx.abort(error).
No mutation of context primitives or timeline state occurs in enforce mode.
Use enforce mode when:
- Debugging pipeline transformations and verifying that upstream stages emit conformant history.
- Running CI conformance tests against custom executors or synthetic conversation traces.
2. action: 'mutate' (Best-Effort Auto-Repair)
mutate is the pragmatic mode: instead of dropping a live conversation on the floor over a sequence glitch, it actively repairs violations that have unambiguous, deterministic, and content-preserving fixes.
When violations are detected in mutate mode:
- The guard classifies violations into repairable and unrepairable sets.
- Safe repair strategies are applied to the timeline and context state.
- The guard re-evaluates the repaired effective timeline.
- If all blocking violations are resolved, execution continues (
next()). - If unrepairable violations remain (e.g. lost historical context), the guard invokes
onViolation(nack,abort, orthrow) for the unrepaired subset.
Repairs applied during mutate mode are stored on ctx.stash under __orderingGuardLastResult (an OrderingGuardResult object) for downstream observability.
Repair Strategies by Rule Type
This table is an honesty document. It spells out exactly which protocol violations this battery can safely paper over and which ones it flatly refuses to touch. That refusal is deliberate: silently fabricating a plausible-looking fix for lost conversation history or wedged primitives would be far worse than rejecting the dispatch outright.
| Rule Type | Strategy in mutate Mode | Repairable? | Concrete Execution Behavior |
|---|---|---|---|
OrderRule | reorder | Yes | Shifts the offending primitive's createdAt (via the matching ctx.mutate*) so it sorts immediately before the primitive it must precede, then re-evaluates the repaired timeline to confirm the fix holds. This reaches the real turn state — an adapter's next history assembly sees the corrected order directly, nothing further to consume. |
AlternationRule | insert-alternation-filler | Yes | Inserts a synthetic Message filler in the opposite role between consecutive same-role messages via ctx.storeMessage, with a neutral acknowledgement as its content. Fillers are scaffolding for ONE dispatch: each pass deletes the previous pass's fillers (via ctx.deleteMessage) and excludes any that remain from the timeline it evaluates, so they can never become repair inputs to themselves. |
RequiredMetadataRule | fill-required-metadata | Opt-in | Fills missing vendor metadata with a documented fallback sentinel. Authorized either by the rule itself (fallbackRepairAuthorized: true — set where the fallback is a value the VENDOR publishes, as on Gemini's thought_signature_required) or globally by allowMetadataFallbackRepair: true for a rule that does not. Both still require action: 'mutate'. |
PreservationRule | None | No | Never repairable. When historical primitives or payload fields are dropped or altered upstream, the guard cannot invent lost context. Rejects dispatch when severity: 'blocking'; the shipped profiles are advisory. |
AdjacencyRule | reorder-adjacent | Yes | Shifts the disallowed successor's createdAt so it sorts immediately BEFORE the primitive it may not follow, then re-evaluates. Every primitive survives — only relative position changes, where dropping the offending message would lose content the caller meant to send. |
RoleRemapRule | N/A | N/A | Wire role remapping hints are adapter-level concerns, not pipeline ordering mutations. |
StaleContentAdvisoryRule | N/A | N/A | Never blocks. Advisory rules produce informational notices only; no repair needed. |
IdentifierUniquenessRule | renumber-colliding-ids | Yes | Renames every member of a collision group through the context's atomic group operation, then re-evaluates. Persistence commits before local state changes, so a throwing store leaves the context exactly as it was rather than half-renamed. Requires the context's group-replacement capability; absent it, the repair is not attempted and the finding stays unrepaired (a blocking collision then nacks). Dispatch surface only — the shipped profile sets surface: 'dispatch', so the surface filter removes it on the turn path entirely: it neither reports nor repairs there. A consumer who sets surface: 'both' gets turn-level detection, where the turn guard reports and, in mutate mode, materialises this strategy. |
Caveats for Mutate Mode
When configuring action: 'mutate', keep the following behavioral boundaries in mind:
- Scope of
OrderRuleRepairs: WhenOrderRulerepairs an ordering mismatch (such as placing thoughts before tool calls), the guard shifts the offending primitive'screatedAtby calling the matchingctx.mutateToolCall/mutateThought/mutateMessagedirectly, then re-evaluates the repaired timeline to confirm the fix actually resolves the violation. Because this reaches the realctx.turnMessages/turnThoughts/turnToolCallsstate — not just an in-memory copy — any LLM adapter's own history assembly sees the corrected order on its next pass without reading anything fromctx.stash.ctx.stash.get('__orderingGuardEffectiveTimeline')remains available purely for observability, if you want to inspect exactly what the guard did on a given iteration. - Metadata Fallback Provenance:
action: 'mutate'alone will not INVENT vendor metadata. Fabricating a vendor signature is a direct provenance claim about where reasoning originated — it is the one place this battery could convincingly lie on your behalf, and it requires the explicitallowMetadataFallbackRepair: trueopt-in. A rule may waive that for its own fallback withfallbackRepairAuthorized: true, which is reserved for values the vendor itself publishes for replayed history: a sentinel is not a forged signature, it is a documented way of stating that no signature exists. See Gemini Thought Sentinels for details. renumber-colliding-idsrewrites durable ids. Whenmutaterepairs atool_call_id_uniquenesscollision, it renames every member of the group and persists the renamed calls. Anything you have recorded against a call id — logs, traces, join tables, your own spool keys — can point at an id that no longer exists. This is the same class of act as thereorderrepair, which rewritescreatedAton persisted primitives, andaction: 'mutate'is the consent: a consumer who sets it is asking the guard to change turn state. If you need to correlate external records against tool-call ids, preferenforce(which rejects a collision rather than renaming it) or enable thetoolCallIdFilteringress hook so the id is de-collided before it ever reaches durable storage.
See Also
- Validation Hub — Overview of the ordering guard battery and model lookup table.
- Atomic Behaviors — Catalog of the 21 atomic behavior profiles.
- Rule Types Reference — Specification of the twelve declarative rule contracts.
- Gemini Sentinels — Using bypass sentinels and
allowMetadataFallbackRepair.