Provider-private pa4 prompt builder for Anthropic Messages requests.
pa4 mirrors the px7 prompt contract in doctrine, not bytes. Layer 0 is a byte-stable system text block selected by mode, Layer 1 is the deterministic Skills index when present, and volatile late context is inserted as the leading text content block of the latest user message because Anthropic has no developer role on the target model.
cache_control planning is fixed for pa4:
- B1 is placed on the last system block.
- B2 is placed on the content block at
prev_turn_boundary, using a 1-based count over message content blocks after any late-context insertion. If that block is not cacheable, B2 walks backward to the nearest cacheable block at a lower position and is omitted when none exists. - B3 is placed only when more than 15 content blocks exist between the prior message breakpoint position, or B1 when B2 is absent, and the latest user message. When triggered, B3 targets the last content block before the latest user message, then walks backward to the nearest cacheable block at or before that target and is omitted when none exists or when it collides with B2.
Verified Anthropic prompt-caching documentation on 2026-07-08 says thinking, redacted_thinking, and empty text blocks cannot be marked directly with cache_control; pa4 treats those blocks as non-cacheable.
pa4 never emits more than three breakpoints. The fourth Anthropic breakpoint is
reserved by ADR 0037 and adding a planned use for it requires a pa4 to pa5
prompt contract bump. Any future layout or fence-token change is also a pa4 to
pa5 bump. pa4 drops the code-style reviewability sentence after the N=3 A/B
verdict (refs #570), a pa3 to pa4 prompt contract bump. It continues to expose
the bash timeout_ms tool-schema field added in pa3 (refs #575).
Summary
Functions
Build Anthropic-native system and messages with the pa4 cache plan.
The pa4 prompt contract label, mirrored into neutral cache metadata (ADR 0037 D7).
Types
@type input() :: %{ mode: :build | :plan, skills_index: String.t() | nil, messages: [map()], late_context: String.t() | nil, prev_turn_boundary: non_neg_integer() | nil }