Scrubbed extract of
AI-Dev-Platform/ADR/ADR-019-Cost-Governance-Policy.md— cited by the Chronicle (04-the-proving-ground). Redactions: phone / service detail / secrets / clearance / personal / internal hosts.
category: Work project: AI-Dev-Platform status: accepted created: 2026-06-18 number: ADR-019 title: Cost Governance Policy tags:
- adr
- decision-record
- cost-governance
- routing
- omnigent
- meta-harness
- policy
ADR-019: Cost Governance Policy
Status
Accepted — 2026-06-18
Context
[[ADR-020-Meta-Harness-Omnigent-Mapping]] canonicalizes a five-phase plan to promote project-level abstractions into platform-level contracts. Phase 1 — “Policy Decoupled from Prompt” (IP-OMNIGENT-001 Phase 1) — targets the model-cost-guard skill.
Today cost governance lives entirely in the agent’s context window. The model-cost-guard skill (in bears-eat-beets) encodes a watcher table — session-token warn/stop thresholds, per-request budget overrun, per-tier saturation, and provider quota headroom — as prompt text. That has three structural problems:
- Cost-of-cost. Every governed session pays tokens to carry the policy. The policy competes for context with the actual task.
- Unenforceable cross-harness. A prompt-level guard only governs the harness whose prompt contains it. A meta-harness ([[ADR-020-Meta-Harness-Omnigent-Mapping]]) routing across MAF, Claude Code, and Codex cannot enforce one budget from inside any single prompt.
- Invisible to the control plane. ADP-Orchestration-UI cannot render budget state it cannot read; a threshold buried in a prompt is not a queryable service-layer fact.
The platform already has a routing seam in AIService.Core: TierResolver resolves a session’s active ModelTier (Local / Budget / Mid / Premium), enforcing the [[ADR-015-Sensitive-Data-Classification-And-Handling]] §3b invariant that a session with a sensitive document is locked to the Local tier. RoutingUseCase consumes that resolution. This is the natural enforcement point for cost policy — it sits before any ILLMProvider dispatch and is already inside the hexagonal Core.
Decision
Promote the model-cost-guard watcher thresholds into a CostGovernancePolicy domain value object in AIService.Core, and consult it inside TierResolver before dispatch. The skill becomes a read-only client that surfaces policy state to agents; it no longer enforces it.
The CostGovernancePolicy type
CostGovernancePolicy is an immutable record in AIService.Core/Domain/ValueObjects/. It captures the five thresholds from the model-cost-guard watcher table, validated at construction via a Create static factory (a private constructor guarantees every instance is well-formed), with a canonical Default baseline mirroring the historical table:
public sealed record CostGovernancePolicy
{
public decimal SessionBudgetWarnThreshold { get; } // 0.75 — warn at 75% of session token budget
public decimal SessionBudgetStopThreshold { get; } // 0.95 — hard-stop + notify at 95%
public decimal PerRequestBudgetDowngrade { get; } // 1.25 — downgrade one rung at 125% of per-request budget
public decimal TierSaturationDowngrade { get; } // 0.90 — downgrade one rung at 90% tier capacity
public decimal ProviderQuotaRouteAway { get; } // 0.10 — route away below 10% provider quota headroom
public static CostGovernancePolicy Default { get; } // = Create(0.75, 0.95, 1.25, 0.90, 0.10)
public static CostGovernancePolicy Create(decimal warn, decimal stop, decimal perRequest,
decimal saturation, decimal quota); // validates ranges; stop must be strictly above warn
public CostGovernanceAction EvaluateSessionBudget(decimal budgetFractionUsed); // Allow | Warn | HardStop
public bool ShouldDowngrade(decimal perRequestBudgetMultiple, decimal tierSaturationFraction);
public bool ShouldRouteAway(decimal providerQuotaHeadroomFraction);
}
Construction validation enforces that all four fraction thresholds sit in their valid range (warn and quota in the exclusive interval (0, 1); stop and saturation in (0, 1]), the per-request multiplier is >= 1.0, and the stop threshold is strictly greater than the warn threshold. Invalid input fails fast with ArgumentOutOfRangeException / ArgumentException rather than producing a misconfigured policy.
The type carries no infrastructure concerns — no [BsonId], no JSON binding attributes. When V1 ships the policy as static YAML configuration, a dedicated infrastructure-layer mapper translates the config shape into CostGovernancePolicy.Create(...), preserving the hexagonal boundary ([[ADR-001-Architecture-Overview]]).
Tier limits and the governance ladder
The thresholds map onto the existing four-tier ModelTier ladder. Downgrade and route-away both move the resolved tier exactly one rung toward the cheapest tier (Premium → Mid → Budget → Local, never below Local), mirroring the model-cost-guard “move exactly one rung” constraint. The session-budget ladder is independent of tier and gates dispatch outright:
| Signal | Threshold (default) | Action |
|---|---|---|
| Session tokens used | >= 0.75 |
Warn (no tier change) |
| Session tokens used | >= 0.95 |
Hard-stop + notify operator |
| Per-request cost multiple | >= 1.25 |
Downgrade one rung |
| Tier in-flight saturation | >= 0.90 |
Downgrade one rung |
| Provider quota headroom | < 0.10 |
Route away (downgrade one rung) |
Integration with routing
TierResolver gains ResolveWithGovernance(SessionContext, CostGovernancePolicy, CostGovernanceSignals), additive alongside the existing ResolveActiveTier(SessionContext) so no current caller breaks. It returns a RoutingDecision(ModelTier Tier, CostGovernanceAction Action, bool Locked). The precedence is invariant-first:
- The [[ADR-015-Sensitive-Data-Classification-And-Handling]] §3b sensitive-document lock is evaluated first. A sensitive session is pinned to
Localand is exempt from every cost-driven tier change. The budget action is still surfaced (so the operator sees budget state), but the tier never moves offLocal. - For non-sensitive sessions, a session-budget
HardStopshort-circuits dispatch — the decision returnsHardStopand the caller refuses to route and notifies, rather than silently downgrading. - Otherwise, route-away (low provider quota) or downgrade (cost overrun / tier saturation) moves the resolved tier one rung down.
CostGovernanceSignals (session-budget fraction used, per-request budget multiple, tier saturation fraction, provider quota headroom) is the live input set captured per routing decision; a None baseline (full headroom, zero pressure) lets callers resolve without live signals when none are available, in which case every governance check passes.
Consequences
Positive
- Policy leaves the prompt. Cost governance is a service-layer fact, queryable by the control plane and enforceable across every harness the meta-harness routes to.
- Token savings. Governed sessions no longer carry the watcher table in context.
- Invariant safety preserved. The ADR-015 §3b sensitive lock takes strict precedence over cost optimization; a sensitive session can never be cost-downgraded off the Local tier.
- Backwards compatible.
ResolveWithGovernanceis additive; the existingResolveActiveTierpath and its callers are untouched. Hard-stop is explicit and observable, never a silent halt. - Hexagonal boundary intact.
CostGovernancePolicyis a pure domain value object; configuration enters via an infrastructure mapper, not by coupling Core to YAML or persistence.
Negative
model-cost-guardrole change. Consumers that treat the skill as an enforcement mechanism must now treat it as a read-only observer. This is a behavioral change for running sessions (also called out in [[ADR-020-Meta-Harness-Omnigent-Mapping]] Consequences §2).- Signal sourcing is deferred.
CostGovernanceSignalsdefines the contract, but live per-request cost, tier in-flight counts, and provider quota headroom are not yet wired from telemetry. Until they are, callers passCostGovernanceSignals.Noneand only the session-budget ladder (fed by existing token accounting) is live. This is intentional staging, not a gap to be hidden. - Static V1 only. V1 ships a single static policy. Per-account / per-project policy resolution and dynamic thresholds are future work, aligned with the [[ADR-MLOPS-001-Router-Model-Architecture]] V1 → V2 progression.
Risks and Mitigations
| Risk | Mitigation |
|---|---|
| A cost downgrade silently degrades output quality below task needs | Downgrade moves exactly one rung and never below Local; hard-stop is explicit and notified. The model-cost-guard skill surfaces the decision so the operator sees it. |
| Sensitive session escapes the Local tier via a cost path | ResolveWithGovernance evaluates the ADR-015 §3b lock before any cost logic; a unit test (SensitiveSession_LockedToLocal_EvenUnderMaxPressure) asserts it under maximal pressure. |
| Misconfigured policy (stop below warn, out-of-range fraction) routes incorrectly | Create validates ranges and ordering, failing fast at construction; tests cover every rejection path. |
Related Decisions
- [[ADR-020-Meta-Harness-Omnigent-Mapping]] — Phase 1 of the Omnigent adoption sequence; this ADR is its implementation record.
- [[ADR-001-Architecture-Overview]] — Hexagonal architecture;
CostGovernancePolicyis a Core domain value object, configuration enters via an infrastructure mapper. - [[ADR-010-Dual-Gate-MAF]] —
CostGovernancePolicyfeeds the existingModelRoutingService/TierResolver, not a new gate type. - [[ADR-015-Sensitive-Data-Classification-And-Handling]] — §3b sensitive-document Local lock takes strict precedence over all cost-driven tier changes.
- [[ADR-MLOPS-001-Router-Model-Architecture]] — the V1 static policy router is the same artifact framed here for cost governance.
References
- Task:
obsidian-sync/Tasks/Work/implement-cost-governance-policy-aiservice.md - Source:
omnigent_mapping_analysis.md§1 “Policy Decoupled from Prompt” - Skill:
bears-eat-beets/catalog/skills/model-cost-guard/SKILL.md - Implementation:
AIService.Core/Domain/ValueObjects/CostGovernancePolicy.cs,AIService.Core/Application/Routing/TierResolver.cs