Story-1716 Cost (a/2) — cached role-agnostic prefix + serialization + version-check
RFD-AGT-002 D-9. Atlas's per-turn prompt splits in two: a stable prefix (system text + capability universe + the role-agnostic Product Catalog) that is identical for every counsel turn and so can be served from the provider's prefix cache, and a dynamic suffix (Page Context · the user's role · resolved permissions · the message) that is never cached. This Story builds the cached half: building it deterministically, fingerprinting it, refusing a stale one, and gating the DeepSeek default on the D-5 refusal floor. Sibling cost child (the functions dispatcher: per-session budget + debounce + ADR-130 bound) is out of scope here.
The split (RFD-AGT-002 §3.1)
[ STABLE PREFIX ] + [ DYNAMIC SUFFIX ] system + capability universe Page Context · the user's role · + Product Catalog (role-agnostic) resolved permissions · the message —— CACHED, shared across turns —— —— never cached, per turn ——
The prefix carries NO role, NO permissions, NO power-integer — the role filter + authz run live in the uncached suffix (§3.2). So one warm cache is reused across turns and firms.
AC1 — role-agnostic cached prefix with deterministic serialization (cache-hit reproducible)
ProductCatalog/CatalogSurface/CatalogToolmodel the stable app knowledge. A tool'sneedsis a permission string (matters:write) ornull— a validator rejects a power-integer ("3"), so the cached prefix can never become a second authority map (§3.2 "There is no power-integer").canonical_catalog_json()is the deterministic-serialization gate: sorted keys, surfaces/tools/columns sorted by key, compact separators, no timestamps, no randomness.content_sha256fingerprints the exact prefix bytes the provider caches on;cache_key=universe_version/catalog_version/<digest>.- Reproducible: two builds from equal content — in any source order — yield byte-identical prefix text and the same SHA. A content change (system text or catalog) changes the SHA, forcing a re-warm.
AC2 — per-turn catalog-version check rejects a stale prefix
assert_prefix_fresh(prefix, expected_catalog_version=…, expected_universe_version=…)runs every turn.- A mismatch on either axis raises
AtlasStalePrefixError(codeatlas_stale_prefix) — fail loud, because a stale prefix silently serves removed/renamed capabilities (§3.1 cache gate 2). - Three version axes are kept distinct (§7.4 locked):
universe_version(closed capability enum, D-1) andcatalog_version(the index, D-2/D-9) ride the cached prefix;contract_version(one surface's Page Card, D-10) is live Page Context and deliberately stays in the uncached suffix.
AC3 — DeepSeek used only if it passes the D-5 refusal floor
assert_model_meets_refusal_floor(model_id, …)gates the DeepSeek default: when floor enforcement is on, adeepseek/*brain may run only if an attestation records it cleared the D-5 refusal floor.- No auto-downgrade to a cheaper model — the gate raises
AtlasRefusalFloorError(D-9 "never fail cheap"). A frontier (non-DeepSeek) tier is not governed by this gate. - Attestation + enforcement are env-driven (
ATLAS_DEEPSEEK_REFUSAL_FLOOR,ATLAS_ENFORCE_REFUSAL_FLOOR). Enforcement defaults OFF in v1: the full refusal harness gates GA, not the first authorized write (D-5 / D-11), so the mechanism ships now and is flipped on at GA. Wired non-bricking intoconfig.load.
Files
agents/atlas/cost_prefix.py— catalog models,build_cached_prefix(),canonical_catalog_json(),assert_prefix_fresh(),assert_model_meets_refusal_floor().agents/atlas/errors.py—AtlasStalePrefixError,AtlasRefusalFloorErroradded to the typed taxonomy.agents/atlas/config.py— floor gate wired intoload()(default no-op).agents/atlas/tests/test_cost_prefix.py— 17 deterministic, model-free tests.
Verification
ruff check agents/→ all checks passedcd agents/atlas && pytest tests/test_cost_prefix.py -q→ 17 passedpytest tests -q→ 326 passed (no regression from the config wiring)