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 role filler (with id and content set to __ordering-guard-filler-<id1>-<id2>) between consecutive same-role messages via ctx.storeMessage. |
RequiredMetadataRule | fill-required-metadata | Opt-in | Fills missing vendor metadata (e.g. Gemini thoughtSignature) with a documented fallback sentinel. Requires double opt-in: both action: 'mutate' AND allowMetadataFallbackRepair: true. |
PreservationRule | None | No | Never repairable. When historical primitives or payload fields are dropped or altered upstream, the guard cannot invent lost context. Rejects dispatch. |
AdjacencyRule | None | No | Never repairable. Wedged or interleaving primitives cannot be safely dropped or reordered without risking causal intent. Rejects dispatch. |
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. |
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 forge or fill missing 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. For that reason, missing metadata repair is never enabled bymutatealone; it requires an explicit, separateallowMetadataFallbackRepair: trueopt-in. See Gemini Thought Sentinels for details.
See Also
- Validation Hub — Overview of the ordering guard battery and model lookup table.
- Atomic Behaviors — Catalog of the 16 atomic behavior profiles.
- Rule Types Reference — Specification of the seven declarative rule contracts.
- Gemini Sentinels — Using bypass sentinels and
allowMetadataFallbackRepair.