Skip to content
6 min read · 1,159 words

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.

  • enforce is 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.
  • mutate is what you actually ship. A real user mid-conversation does not care that a vendor's chat template stamped a Thought a millisecond after its ToolCall — they care that their agent kept working. mutate repairs 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:

  1. orderingGuardDispatchMiddleware — Evaluates turn state on every dispatch iteration within the execution loop. Rejects violations via ctx.nack(error).
  2. orderingGuardTurnMiddleware — Evaluates turn state once before the execution loop begins. Halts violations via ctx.abort(error).
ts
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

ts
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 via ctx.nack(error) (or throws E_ORDERING_VIOLATION if onViolation: 'throw').
  • In orderingGuardTurnMiddleware (which has no nack capability), execution is halted via ctx.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:

  1. The guard classifies violations into repairable and unrepairable sets.
  2. Safe repair strategies are applied to the timeline and context state.
  3. The guard re-evaluates the repaired effective timeline.
  4. If all blocking violations are resolved, execution continues (next()).
  5. If unrepairable violations remain (e.g. lost historical context), the guard invokes onViolation (nack, abort, or throw) 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 TypeStrategy in mutate ModeRepairable?Concrete Execution Behavior
OrderRulereorderYesShifts 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.
AlternationRuleinsert-alternation-fillerYesInserts a synthetic Message role filler (with id and content set to __ordering-guard-filler-<id1>-<id2>) between consecutive same-role messages via ctx.storeMessage.
RequiredMetadataRulefill-required-metadataOpt-inFills missing vendor metadata (e.g. Gemini thoughtSignature) with a documented fallback sentinel. Requires double opt-in: both action: 'mutate' AND allowMetadataFallbackRepair: true.
PreservationRuleNoneNoNever repairable. When historical primitives or payload fields are dropped or altered upstream, the guard cannot invent lost context. Rejects dispatch.
AdjacencyRuleNoneNoNever repairable. Wedged or interleaving primitives cannot be safely dropped or reordered without risking causal intent. Rejects dispatch.
RoleRemapRuleN/AN/AWire role remapping hints are adapter-level concerns, not pipeline ordering mutations.
StaleContentAdvisoryRuleN/AN/ANever 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:

  1. Scope of OrderRule Repairs: When OrderRule repairs an ordering mismatch (such as placing thoughts before tool calls), the guard shifts the offending primitive's createdAt by calling the matching ctx.mutateToolCall/mutateThought/mutateMessage directly, then re-evaluates the repaired timeline to confirm the fix actually resolves the violation. Because this reaches the real ctx.turnMessages/turnThoughts/turnToolCalls state — not just an in-memory copy — any LLM adapter's own history assembly sees the corrected order on its next pass without reading anything from ctx.stash. ctx.stash.get('__orderingGuardEffectiveTimeline') remains available purely for observability, if you want to inspect exactly what the guard did on a given iteration.
  2. 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 by mutate alone; it requires an explicit, separate allowMetadataFallbackRepair: true opt-in. See Gemini Thought Sentinels for details.

See Also