Atomic Behaviors Catalog
Rather than duplicating monolithic rule sets for every model vendor, the validation battery defines 16 atomic behavior profiles. Model family recipes compose these granular profiles as pure data. The alternative — maintaining one giant bespoke rule blob per vendor — is how codebases end up with 40 copy-pasted files that silently drift out of sync the first time someone fixes a bug in one and forgets the other 39.
The 16 Atomic Profiles
1. 'permissive'
An empty rule set (rules: [], permissive: true). This is not a coverage gap in the battery — it captures the deliberate, documented design choice by xAI Grok to impose zero role-order limitations on conversation history.
2. 'openai_shape_baseline'
The baseline tool-calling guard for OpenAI-compatible conversations. Enforces that a Message primitive may not immediately follow a ToolCall primitive, because tool execution results in this ADK are stored directly on ToolCall itself.
3. 'strict_alternation'
Enforces that conversation messages alternate strictly between user and assistant roles (roles: ['user', 'assistant'], mode: 'strict').
Field note: the trailing-assistant turn that no one rejects, until one model just stops
A violation of this rule doesn't always look like a 400. ADK's own "Punching Above Its Weights" showcase agent synthesizes plan/nudge Thoughts and injects them into the model-facing prompt as assistant-role content — generated in response to the user's turn, so they land last. The prompt ends on an assistant turn. Gemma and DeepSeek shrug and keep generating past it. qwen3-coder-next's chat template treats a trailing assistant message as a completed turn and stops immediately — no error, no violation, just eval_count: 1, empty content, and a dispatch loop that spins hundreds of times waiting for an answer that will never come. The fix wasn't a repair rule; it was a per-model opt-out that keeps synthetic thoughts out of the model-facing prompt (they still render in the UI) for chat templates known to end a turn on a trailing assistant message. strict_alternation catches the well-behaved failure mode — roles actually out of order. It says nothing about a technically alternating sequence whose last entry is the wrong role for a specific model's template, and that gap doesn't produce a rejectable violation at all: it produces silence.
Field note: the same trailing-assistant turn, at production scale, across three vendors
This isn't a showcase-app curiosity. A real production CI code-review agent hit the exact same shape twice, independently, and had to fix it in production. In one incident, a corrective "nudge" message followed by a synthesized echo Thought left the wire request ending on an assistant turn. Nova/Bedrock didn't reject it: it returned a well-formed 200 with content: null and finish_reason: "stop", byte-identical across four straight retries, silently burning the whole retry budget on a request the model was never going to answer. Other vendors' translators reject this same shape outright as a hard 400 ("Requests ending with a model turn are not supported") — so the failure isn't a Nova defect, it's undefined behavior that different serving stacks resolve differently, some loudly and some not at all. The fix there was structural: append a minimal, inert trailing message after any synthesized assistant-role content so the wire array always ends on a non-assistant turn.
The second incident was a variant of the same root problem with sharper numbers: a model that had already finished its real work re-presented with an unchanged prompt returned empty generations 30/30 times in a live-replay measurement (a clean reproduction of a real production outage) — not because anything was malformed, but because asking a model to repeat itself when it has nothing left to say is, itself, an ordering failure of a subtler kind. Injecting one corrective turn and re-presenting the model's own prior output as a Thought rather than a Message dropped that failure rate from 30/30 to 8/30 in the same live measurement. This battery's strict_alternation profile is the validation-time shape of exactly this lesson: a request that alternates roles by the letter of the rule can still end on the wrong note in the eyes of the specific model reading it, and the only way anyone found that out was watching it fail in production, repeatedly, at scale, until it wasn't a mystery anymore.
4. 'single_tool_call_per_turn'
Enforces a cardinality cap of at most one ToolCall per assistant role group (maxPerGroup: 1), matching Meta Llama 3's non-parallel tool-calling constraint.
5. 'thinking_before_tool_use'
Enforces that within the active assistant turn (onlyLatestGroup: true), any Thought primitive must precede any ToolCall primitive.
6. 'thought_signature_required'
Requires the first ToolCall in an assistant group to carry a thoughtSignature in its payload. It includes fallback sentinel metadata for automated repair when action: 'mutate' and metadata fallback repair are both enabled.
7. 'thought_signature_advisory'
The advisory variant of 'thought_signature_required' (severity: 'advisory'). Evaluates the presence of thoughtSignature for Gemini 2.5 without blocking dispatch.
8. 'function_response_adjacency'
Enforces Gemini's tool-sequence adjacency rule: a Message may not immediately follow a ToolCall primitive before function execution flow concludes.
9. 'full_history_preservation'
Parameterized profile factory (full_history_preservation:<kind>). Stateful preservation check ensuring that historical primitive counts ('toolCall', 'thought', or 'message') never decrease across dispatch iterations. This exists because vendor chat templates have shipped bugs in production that silently dropped historical tool calls or reasoning traces from turn to turn. Used in recipes as 'full_history_preservation:toolCall' or 'full_history_preservation:thought'.
10. 'payload_field_preservation'
Parameterized profile factory (payload_field_preservation:<field>). Stateful check ensuring that specific opaque metadata fields (such as Anthropic thought signatures or GLM clear_thinking flags) remain identical across dispatch iterations, guarding against template regressions that strip vendor-critical annotations. Used in recipes as 'payload_field_preservation:<field>'.
11. 'reasoning_pruned_after_latest_turn'
Implements Qwen 3's reasoning retention invariant: reasoning predating the latest non-tool-call user turn may be pruned, but all reasoning generated at or after that boundary must remain intact and stable.
12. 'stale_thinking_advisory'
Implements Gemma 4 hygiene recommendations. Emits a non-blocking advisory if thinking content older than the latest user turn is resent in history.
Field note: this isn't theoretical — a shipped agent enforces it as a hard rule
The Gemma model card's guidance against resending prior-turn thinking isn't an edge case someone might hit. ADK's own "Punching Above Its Weights" showcase agent (Gemma-4 via LiteRT-LM) treats it as load-bearing: a prior turn's model-generated reasoning is stripped from every subsequent turn's prompt, full stop — no advisory, no opt-out, because the code comment citing it names the exact clause ("Gemma model card §3 — 'No Thinking Content in History'"). Only harness-authored synthetic thoughts (the plan, nudge corrections) survive replay, via an explicit allow-list, precisely because they aren't the model's own past chain-of-thought. This profile ships as advisory — informational, never blocking — because the vendor guidance itself is a recommendation, not a spec-enforced rejection. A caller who has actually watched a Gemma-family model degrade on stale thinking may reasonably decide this specific rule deserves to gate dispatch in their own pipeline rather than just warn.
Field note: found while debugging something else entirely
A real production CI code-review agent discovered this exact failure class sideways, while root-causing an unrelated empty-generation incident. Every LLM battery it used defaulted to replaying every self-authored plain-text thought from every prior turn, forever, unbounded — and none of its own harness-authored notices carried the metadata needed to opt out of that replay. The fix was to keep only the single most recent self-authored thought per turn instead of accumulating the whole history of them, explicitly called out in the commit as "the recommended posture against the unbounded-replay degradation some model families (e.g. Gemma) exhibit." Nobody set out to build that fix. It fell out of chasing a model that kept returning empty responses, and the actual root cause turned out to be a pile of the model's own stale reasoning it had never been told to stop re-reading. The advisory in this profile is the polite version of that lesson; the production incident is what happens when nobody's listening to it.
13. 'role_remap_split_tool_roles'
Requires every ToolCall to carry a producer-set role-remap marker confirming it has been rendered under Granite 3.x's distinct tool-call and tool-response wire roles. IBM shipped two incompatible wire-role schemes across consecutive model generations for the exact same underlying concept, which is why there are two separate profiles here rather than one with a version flag.
14. 'role_remap_inline_tool_call'
Requires every ToolCall to carry a producer-set role-remap marker confirming it has been rendered under Granite 4.x's inline tool-call, remapped-response wire roles.
15. 'harmony_commentary_channel'
Validates the OpenAI GPT-OSS Harmony format. Requires every function ToolCall to carry a payload.channel field, matching Harmony's commentary-channel tagging requirement.
16. 'converse_text_before_tool_use'
AWS Bedrock Converse hosting rule: within an assistant turn, all text Message primitives must precede any ToolCall primitives.
A Known Gap: Stateful Primitive IDs Are Not an Ordering Concern (Yet)
The 16 profiles above validate sequence: what kind of primitive comes before, after, or adjacent to what. None of them validate identity continuity — whether a primitive that plays a stateful role across turns (a book-end plan, a nudge correction, any harness-synthesized control primitive a caller's own dispatch loop depends on being re-seeded turn after turn) keeps a consistent, non-colliding id as it's persisted and replayed.
A validation library that implies completeness it doesn't have is worse than one that admits a hole. We document this limitation plainly because it represents an unmodeled failure mode, not a solved problem.
Field note: an id collision can silently disable a book-end contract
This gap surfaced building ADK's own "Punching Above Its Weights" showcase agent, which opens every turn with a synthetic planning Thought and validates against that plan at turn close — a book-end contract. The in-turn id used to de-duplicate that thought while the turn is live (e.g. __plan-thought) is stable on purpose, so the live UI and the in-turn record map can find it. But that same stability becomes a liability at persistence time: if the thought were ever stored under that in-turn id instead of a freshly minted one, the next turn's history fetch would return a thought already carrying id __plan-thought — and the injection guard that decides whether to seed a fresh plan thought for the new turn (!seededIds.has(PLAN_THOUGHT_ID)) would see that id as already present and skip re-injection. The planner would silently stop running from turn 2 onward. No violation fires. No rule in this battery — not full_history_preservation, not reasoning_pruned_after_latest_turn — is shaped to catch it, because nothing was dropped or reordered; a primitive's identity just leaked across a boundary it was never supposed to cross. The fix was disciplined at the source (mint a fresh id at persist time, never reuse the in-turn key), not a repair strategy — there currently isn't one.
Field note: the same shape, at higher stakes, in a real production panel
A stateful-id collision doesn't need a book-end contract to bite — it just needs two things that were supposed to stay distinct sharing an identity they shouldn't. A real production CI code-review agent measured exactly this: an orchestrator authored three genuinely distinct findings in one turn, and a downstream identity-collapse step folded all three under one reused id (the model, having been burned once by a rejected empty-string id, learned that a specific placeholder string was "accepted" and then reused that same placeholder for every finding that turn). Two of the three were silently discarded before they ever reached a human reviewer — a disposition gate built specifically to stop unbacked assertions instead dropped authored, backed ones, for a reason that had nothing to do with their content. The fix made identity issuance server-authoritative: an id is only honored as "already claimed" if the harness itself issued it and nothing earlier in the same batch already claimed it first, so a model reusing, guessing, or omitting an id always mints something fresh rather than colliding with someone else's. The failure direction was inverted on purpose — from "silently drop the duplicate" to "when in doubt, keep both." This battery's Known Gap is that same lesson at the primitive level: nothing here yet stops a caller's own stateful id from being replayed into a context it doesn't belong in, silently discarding whatever was supposed to live at that identity instead.
This is flagged here deliberately, not quietly worked around: it is a real, observed failure class that the current OrderingRule vocabulary does not model. If your own dispatch loop relies on a stateful control-plane primitive surviving replay by id, that continuity is on you to defend today — the battery does not yet have a rule shape for it.
See Also
- Validation Hub — Overview and model-to-recipe lookup table.
- Rule Types Reference — Full declarative schema for all seven rule variants.
- Family Recipes Catalog — Matrix of all 38 pre-configured family recipes composing these behaviors.
- Writing a Profile — How to create new atomic profiles and recipes.