---
url: 'https://adk.nht.io/batteries/validation/behaviors.md'
description: >-
  Catalog of the 16 atomic ordering behavior profiles in the validation battery,
  detailing the invariants each profile enforces.
---

# Atomic Behaviors Catalog

## LLM summary — Atomic Behaviors Catalog

* The validation battery defines **16 atomic behavior profiles**. Model family recipes compose these atomic profiles rather than defining bespoke logic.
* Profiles catalog:
  * `permissive`: True unconstrained ordering (`rules: []`, `permissive: true`).
  * `openai_shape_baseline`: `AdjacencyRule` forbidding `message` immediately after `toolCall` (tool results live on `ToolCall`).
  * `strict_alternation`: `AlternationRule` enforcing strict `user`/`assistant` cycling.
  * `single_tool_call_per_turn`: `AlternationRule` with `maxPerGroup: 1` (Llama 3 non-parallel tool constraint).
  * `thinking_before_tool_use`: `OrderRule` ensuring `thought` precedes `toolCall` in the latest assistant group.
  * `thought_signature_required`: Blocking `RequiredMetadataRule` requiring `thoughtSignature` on first `toolCall` in group.
  * `thought_signature_advisory`: Non-blocking `RequiredMetadataRule` (`severity: 'advisory'`) for Gemini 2.5.
  * `function_response_adjacency`: `AdjacencyRule` forbidding `message` immediately after `toolCall` in Gemini sequences.
  * `full_history_preservation`: Parameterized `PreservationRule` (`kind`) ensuring non-decreasing counts.
  * `payload_field_preservation`: Parameterized `PreservationRule` (`payloadField`) ensuring stable metadata fields.
  * `reasoning_pruned_after_latest_turn`: `PreservationRule` ensuring recent thoughts remain stable while permitting historical pruning.
  * `stale_thinking_advisory`: Non-blocking `StaleContentAdvisoryRule` against resending thoughts older than latest user turn.
  * `role_remap_split_tool_roles`: `RoleRemapRule` confirming a Granite 3.x role-remap marker is present.
  * `role_remap_inline_tool_call`: `RoleRemapRule` confirming a Granite 4.x role-remap marker is present.
  * `harmony_commentary_channel`: `RequiredMetadataRule` requiring a `channel` payload field on every `ToolCall`, matching GPT-OSS's Harmony commentary-channel tagging.
  * `converse_text_before_tool_use`: `OrderRule` ensuring `message` text precedes `toolCall` in Bedrock Converse turns.
* **Known gap:** none of the 16 profiles validate stateful primitive *id* continuity across turns (e.g. a book-end control primitive silently failing to re-seed because it was persisted under an in-turn-only id) — see "A Known Gap" below.

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'`).

:::: info 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 `Thought`s 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.
::::

:::: info 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.

:::: info 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.
::::

:::: info 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.

:::: info 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.
::::

:::: info 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](./index)** — Overview and model-to-recipe lookup table.
* **[Rule Types Reference](./rule-types)** — Full declarative schema for all seven rule variants.
* **[Family Recipes Catalog](./recipes)** — Matrix of all 38 pre-configured family recipes composing these behaviors.
* **[Writing a Profile](./writing-a-profile)** — How to create new atomic profiles and recipes.
