Writing an Ordering Profile & Family Recipe
In the @nhtio/adk validation battery, an ordering profile is pure declarative data.
You do not write procedural loops over message arrays or imperative timestamp comparisons. You declare what invariant must hold over the conversation timeline using the battery's declarative rule types, and the shared evaluation engine and middleware handle validation, snapshotting, repair, and error reporting.
When adding support for a new model or model generation, determine which tier of work is required:
- Tier 1: Composing a Family Recipe (the common case) — Add an entry to
families.tscomposing existing atomic behaviors. - Tier 2: Adding an Atomic Behavior Profile — Create a new
.tsfile inprofiles/when a model has an ordering constraint that maps onto one of the seven rule types but does not yet exist in the catalog. - Tier 3: Extensibility Escape Hatches — Pass ad-hoc profiles in consumer code or extend the rule type union.
1. Adding a Family Recipe (The Common Case)
Before authoring a new file, check src/batteries/validation/profiles/families.ts.
Model family recipes represent real-world model deployments (e.g. anthropic-manual-thinking, gemini-3, qwen-3). A family recipe names the combination of atomic behaviors that the target model family requires.
The Recipe Syntax
In src/batteries/validation/profiles/families.ts, add an entry to FAMILY_RECIPES:
export const FAMILY_RECIPES: Record<string, readonly string[]> = {
// ... existing recipes
'my-new-model-family': [
'strict_alternation',
'openai_shape_baseline',
'full_history_preservation:thought',
],
}Parameterized Behaviors
Two atomic behavior profiles in the battery are factories that accept parameters via a colon (:) separator in FAMILY_RECIPES:
full_history_preservation:<kind>- Parameter:
OrderingPrimitiveKind—'message' | 'thought' | 'toolCall'. - Example:
'full_history_preservation:thought'enforces that historical thoughts are never dropped.
- Parameter:
payload_field_preservation:<field>- Parameter: dot-path into primitive payload (e.g.
signature,clear_thinking,encrypted_content). - Example:
'payload_field_preservation:signature'validates thatThought.payload.signatureremains unchanged across iterations.
- Parameter: dot-path into primitive payload (e.g.
Resolution of these tokens is handled automatically when string profile names are passed into the guard options.
Recipe Authoring Checklist
- Naming: Use lowercase kebab-case (
<family>-<generation-or-mode>, e.g.deepseek-v4,granite-4-x). - Cite Vendor Sources: Add a JSDoc comment above the entry citing the vendor documentation and the date checked.
- Handle Documentation Gaps Honestly: If you are authoring a recipe based on an educated baseline guess rather than a verified spec (as with
bytedance-seedormuse-spark), add an explicit/** UNCONFIRMED baseline guess ... */comment. Never dress up an unverified assumption as certainty — a downstream consumer trusting an unverified recipe inenforcemode in production will discover your guess the hard way.
2. Adding an Atomic Behavior Profile
When a vendor introduces an ordering constraint whose semantics are not yet represented in src/batteries/validation/profiles/, author a new atomic profile.
An atomic behavior file lives in src/batteries/validation/profiles/<behavior_name>.ts and exports an OrderingProfile object.
The Seven Declarative Rule Types
Your profile's rules array contains one or more of the seven discriminated union variants defined in src/batteries/validation/types.ts:
1. OrderRule (type: 'order')
Enforces relative order between two primitive kinds ('message' | 'thought' | 'toolCall').
import type { OrderingProfile } from '../types'
export const thinkingBeforeToolUse: OrderingProfile = {
name: 'thinking-before-tool-use',
description:
'Within the latest assistant turn, thought must precede toolCall.',
rules: [
{
type: 'order',
id: 'thinking-before-tool-use',
before: 'thought',
after: 'toolCall',
scope: 'adjacent-same-role-group', // or 'entire-turn'
onlyLatestGroup: true, // evaluate only the active turn
},
],
}2. RequiredMetadataRule (type: 'requiredMetadata')
Enforces that a primitive carries specific provider metadata in its payload object.
export const thoughtSignatureRequired: OrderingProfile = {
name: 'thought-signature-required',
description:
'The first ToolCall must carry a thoughtSignature in its payload.',
rules: [
{
type: 'requiredMetadata',
id: 'thought-signature-required',
kind: 'toolCall',
applyTo: 'first-in-group', // or 'every'
requiredPayloadKey: 'thoughtSignature',
severity: 'blocking', // or 'advisory'
fallbackPayloadValue: 'skip_thought_signature_validator',
fallbackReplayCompatibility: 'gemini-thought-signature-sentinel-v1',
},
],
}3. AlternationRule (type: 'alternation')
Enforces strict turn-to-turn role alternation and optional tool call cardinality bounds.
export const singleToolCallPerTurn: OrderingProfile = {
name: 'single-tool-call-per-turn',
description:
'Strict alternation with at most one ToolCall per assistant group.',
rules: [
{
type: 'alternation',
id: 'single-tool-call-per-turn',
roles: ['user', 'assistant'],
mode: 'strict',
maxPerGroup: 1, // Caps ToolCalls per turn (e.g. Llama 3)
},
],
}4. AdjacencyRule (type: 'adjacency')
Enforces that disallowed primitive kinds may not immediately follow a specified primitive. In this ADK, tool execution results live directly on ToolCall, so adjacency directly constrains immediate successors without requiring field correlation.
export const openaiShapeBaseline: OrderingProfile = {
name: 'openai-shape-baseline',
description:
'A Message may not immediately follow a ToolCall; tool results live on ToolCall itself.',
rules: [
{
type: 'adjacency',
id: 'message-not-immediately-after-tool-call',
first: 'toolCall',
disallowBetween: ['message'],
},
],
}5. PreservationRule (type: 'preservation')
A stateful check that diffs the current dispatch against the previous iteration's snapshot on ctx.stash.
export const reasoningPrunedAfterLatestTurn: OrderingProfile = {
name: 'reasoning-pruned-after-latest-turn',
description:
'Reasoning before latest user turn may drop; recent reasoning must remain unchanged.',
rules: [
{
type: 'preservation',
id: 'reasoning-pruned-after-latest-turn',
kind: 'thought',
// Invariants: 'count-non-decreasing' | 'payload-field-stable' | 'pruned-after-latest-turn'
invariant: 'pruned-after-latest-turn',
resetOnModelSwitch: false,
},
],
}6. RoleRemapRule (type: 'roleRemap')
Validates custom wire role mappings (e.g. IBM Granite split tool roles).
export const roleRemapSplitToolRoles: OrderingProfile = {
name: 'role-remap-split-tool-roles',
description: 'IBM Granite 3.x wire role mapping validator.',
rules: [
{
type: 'roleRemap',
id: 'granite-3-x-split-tool-roles',
kind: 'toolCall',
variant: 'granite-3.x',
expectedRoleTag: 'payload.roleTag',
},
],
}7. StaleContentAdvisoryRule (type: 'staleContentAdvisory')
Non-blocking hygiene recommendation rule identifying stale content older than the latest user turn.
export const staleThinkingAdvisory: OrderingProfile = {
name: 'stale-thinking-advisory',
description:
'Gemma 4 hygiene rule advising against resending stale thinking.',
rules: [
{
type: 'staleContentAdvisory',
id: 'stale-thinking-gemma4',
kind: 'thought',
scope: 'before-latest-user-turn',
optOutOptionKey: 'preserveThinking',
},
],
}3. Extensibility Escape Hatches
OrderingProfile objects are plain JavaScript objects. You do not need to wait for upstream library releases, fork the package, or file an issue to enforce custom constraints or support a newly released model.
Passing Ad-Hoc Profiles Directly
Consumers can pass ad-hoc profile objects directly into profiles: [...]:
import { orderingGuardDispatchMiddleware } from '@nhtio/adk/batteries/validation'
import type { OrderingProfile } from '@nhtio/adk/batteries/validation'
const myCustomProfile: OrderingProfile = {
name: 'custom-internal-guard',
description: 'Custom internal pipeline constraints.',
rules: [
{
type: 'order',
id: 'custom-thought-order',
before: 'thought',
after: 'toolCall',
scope: 'adjacent-same-role-group',
},
],
}
const middleware = orderingGuardDispatchMiddleware({
profiles: ['nova', myCustomProfile],
action: 'enforce',
})See Also
- Validation Hub — Overview and model selector matrix.
- Atomic Behaviors — Complete catalog of built-in atomic profiles.
- Rule Types Reference — Specification of the seven rule contracts.
- Family Recipes Catalog — Reference list of all 38 family recipes.