use serde_json::json;
use crate::acg::capability::{CacheEconomics, ModelFamilyCapabilities};
use crate::acg::debug as acg_debug;
use crate::acg::profile::StabilityClass;
use crate::acg::prompt_ir::{BlockContentType, PromptBlock, PromptIR, PromptRole, ProvenanceLabel};
use crate::acg::stability::StabilityAnalysisResult;
use crate::acg::types::SharingScope;
impl CacheEconomics {
fn breakeven_reads(&self, write_multiplier: f64) -> f64 {
let cached_read_delta = 1.0 - self.read_multiplier;
if cached_read_delta <= 0.0 {
return f64::INFINITY;
}
(write_multiplier - 1.0) / cached_read_delta
}
fn marginal_net_savings(&self, tokens: u32, expected_reads: f64) -> f64 {
let cached_read_delta = 1.0 - self.read_multiplier;
let write_tax = self.write_short_multiplier - 1.0;
f64::from(tokens) * ((cached_read_delta * expected_reads) - write_tax)
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct EconomicsPlan {
pub planned_breakpoints: Vec<PlannedBreakpoint>,
pub minimum_cacheable_tokens: u32,
pub observed_reuse_horizon: f64,
pub write_5m_breakeven_reads: f64,
pub write_1h_breakeven_reads: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct PlannedBreakpoint {
pub stable_prefix_end: usize,
pub cumulative_tokens: u32,
pub marginal_tokens: u32,
pub expected_reads: f64,
pub expected_net_savings: f64,
pub scope: SharingScope,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CandidateKind {
User,
ToolCluster,
Retrieval,
System,
Structured,
Generic,
}
impl CandidateKind {
fn label(self) -> &'static str {
match self {
Self::User => "user_boundary",
Self::ToolCluster => "tool_cluster_boundary",
Self::Retrieval => "retrieval_boundary",
Self::System => "system_boundary",
Self::Structured => "structured_boundary",
Self::Generic => "generic_boundary",
}
}
fn priority(self) -> u8 {
match self {
Self::User => 5,
Self::ToolCluster => 3,
Self::Retrieval => 4,
Self::System => 2,
Self::Structured => 1,
Self::Generic => 0,
}
}
}
#[derive(Debug, Clone)]
struct BreakpointCandidate {
stable_prefix_end: usize,
cumulative_tokens: u32,
weakest_prefix_signal: f64,
expected_reads: f64,
kind: CandidateKind,
}
#[derive(Debug, Clone, Copy)]
struct PrefixStats {
cumulative_tokens: u32,
weakest_prefix_signal: f64,
}
#[derive(Debug, Clone, Copy)]
struct CandidateSelectionState {
total_value: f64,
previous_candidate_index: Option<usize>,
}
pub(crate) fn plan_breakpoints(
prompt_ir: &PromptIR,
stability: &StabilityAnalysisResult,
observation_count: u32,
capabilities: &ModelFamilyCapabilities,
) -> EconomicsPlan {
let Some(pricing) = capabilities.cache_economics.as_ref() else {
return plan_without_pricing(stability, observation_count, capabilities);
};
let observed_reuse_horizon = observation_count.saturating_sub(1) as f64;
let minimum_cacheable_tokens = capabilities.min_cacheable_tokens.unwrap_or(u32::MAX);
let max_breakpoints = capabilities.max_cache_breakpoints.unwrap_or(0) as usize;
let mut plan = build_economics_plan(pricing, minimum_cacheable_tokens, observed_reuse_horizon);
let skip_reasons = breakpoint_skip_reasons(
observed_reuse_horizon,
max_breakpoints,
stability.stable_prefix_length,
minimum_cacheable_tokens,
);
if !skip_reasons.is_empty() {
acg_debug::emit(
"economics_plan_skipped",
json!({
"reason": skip_reasons,
"model_family": capabilities.model_family,
"observation_count": observation_count,
"observed_reuse_horizon": observed_reuse_horizon,
"stable_prefix_length": stability.stable_prefix_length,
"minimum_cacheable_tokens": minimum_cacheable_tokens,
"max_breakpoints": max_breakpoints,
}),
);
return plan;
}
let stable_prefix_end = stability.stable_prefix_length.min(prompt_ir.blocks.len());
let prefix_stats = build_prefix_stats(
prompt_ir,
stability,
stable_prefix_end,
minimum_cacheable_tokens,
observed_reuse_horizon,
capabilities,
observation_count,
);
let candidates = collect_breakpoint_candidates(
prompt_ir,
&prefix_stats,
stable_prefix_end,
observed_reuse_horizon,
minimum_cacheable_tokens,
capabilities,
);
if candidates.is_empty() {
acg_debug::emit(
"economics_plan_skipped",
json!({
"reason": "no_viable_candidates",
"model_family": capabilities.model_family,
"observation_count": observation_count,
"observed_reuse_horizon": observed_reuse_horizon,
"stable_prefix_length": stability.stable_prefix_length,
"minimum_cacheable_tokens": minimum_cacheable_tokens,
}),
);
return plan;
}
let Some(selected_candidate_indices) =
select_breakpoint_candidates(&candidates, max_breakpoints, pricing)
else {
acg_debug::emit(
"economics_plan_skipped",
json!({
"reason": "non_positive_expected_net_savings",
"model_family": capabilities.model_family,
"observation_count": observation_count,
"observed_reuse_horizon": observed_reuse_horizon,
"stable_prefix_length": stability.stable_prefix_length,
"minimum_cacheable_tokens": minimum_cacheable_tokens,
}),
);
return plan;
};
append_selected_breakpoints(
&mut plan,
&selected_candidate_indices,
&candidates,
pricing,
capabilities,
observed_reuse_horizon,
);
emit_plan_limit_reached_if_needed(&plan, &candidates, max_breakpoints, capabilities);
acg_debug::emit(
"economics_plan_result",
json!({
"model_family": capabilities.model_family,
"observation_count": observation_count,
"observed_reuse_horizon": observed_reuse_horizon,
"minimum_cacheable_tokens": minimum_cacheable_tokens,
"stable_prefix_length": stability.stable_prefix_length,
"planned_breakpoints": plan
.planned_breakpoints
.iter()
.map(|breakpoint| json!({
"stable_prefix_end": breakpoint.stable_prefix_end,
"cumulative_tokens": breakpoint.cumulative_tokens,
"marginal_tokens": breakpoint.marginal_tokens,
"expected_reads": breakpoint.expected_reads,
"expected_net_savings": breakpoint.expected_net_savings,
}))
.collect::<Vec<_>>(),
}),
);
plan
}
fn plan_without_pricing(
stability: &StabilityAnalysisResult,
observation_count: u32,
capabilities: &ModelFamilyCapabilities,
) -> EconomicsPlan {
acg_debug::emit(
"economics_plan_skipped",
json!({
"reason": "missing_cache_economics",
"model_family": capabilities.model_family,
"observation_count": observation_count,
"stable_prefix_length": stability.stable_prefix_length,
}),
);
EconomicsPlan {
planned_breakpoints: Vec::new(),
minimum_cacheable_tokens: capabilities.min_cacheable_tokens.unwrap_or(u32::MAX),
observed_reuse_horizon: observation_count.saturating_sub(1) as f64,
write_5m_breakeven_reads: f64::INFINITY,
write_1h_breakeven_reads: f64::INFINITY,
}
}
fn build_economics_plan(
pricing: &CacheEconomics,
minimum_cacheable_tokens: u32,
observed_reuse_horizon: f64,
) -> EconomicsPlan {
EconomicsPlan {
planned_breakpoints: Vec::new(),
minimum_cacheable_tokens,
observed_reuse_horizon,
write_5m_breakeven_reads: pricing.breakeven_reads(pricing.write_short_multiplier),
write_1h_breakeven_reads: pricing
.write_long_multiplier
.map_or(f64::INFINITY, |multiplier| {
pricing.breakeven_reads(multiplier)
}),
}
}
fn breakpoint_skip_reasons(
observed_reuse_horizon: f64,
max_breakpoints: usize,
stable_prefix_length: usize,
minimum_cacheable_tokens: u32,
) -> Vec<&'static str> {
let mut reasons = Vec::new();
if observed_reuse_horizon <= 0.0 {
reasons.push("reuse_horizon_non_positive");
}
if max_breakpoints == 0 {
reasons.push("max_breakpoints_zero");
}
if stable_prefix_length == 0 {
reasons.push("stable_prefix_empty");
}
if minimum_cacheable_tokens == u32::MAX {
reasons.push("min_cacheable_tokens_unavailable");
}
reasons
}
fn append_selected_breakpoints(
plan: &mut EconomicsPlan,
selected_candidate_indices: &[usize],
candidates: &[BreakpointCandidate],
pricing: &CacheEconomics,
capabilities: &ModelFamilyCapabilities,
observed_reuse_horizon: f64,
) {
let mut previous_cumulative_tokens = 0_u32;
for &candidate_index in selected_candidate_indices {
let candidate = &candidates[candidate_index];
let marginal_tokens = candidate
.cumulative_tokens
.saturating_sub(previous_cumulative_tokens);
let expected_net_savings =
pricing.marginal_net_savings(marginal_tokens, candidate.expected_reads);
if expected_net_savings <= 0.0 || marginal_tokens == 0 {
acg_debug::emit(
"economics_candidate_rejected",
json!({
"reason": if marginal_tokens == 0 {
"zero_marginal_tokens"
} else {
"non_positive_expected_net_savings"
},
"model_family": capabilities.model_family,
"stable_prefix_end": candidate.stable_prefix_end,
"cumulative_tokens": candidate.cumulative_tokens,
"marginal_tokens": marginal_tokens,
"expected_reads": candidate.expected_reads,
"expected_net_savings": expected_net_savings,
"candidate_kind": candidate.kind.label(),
}),
);
continue;
}
plan.planned_breakpoints.push(PlannedBreakpoint {
stable_prefix_end: candidate.stable_prefix_end,
cumulative_tokens: candidate.cumulative_tokens,
marginal_tokens,
expected_reads: candidate.expected_reads,
expected_net_savings,
scope: SharingScope::Session,
});
acg_debug::emit(
"economics_candidate_selected",
json!({
"model_family": capabilities.model_family,
"stable_prefix_end": candidate.stable_prefix_end,
"cumulative_tokens": candidate.cumulative_tokens,
"marginal_tokens": marginal_tokens,
"observed_reuse_horizon": observed_reuse_horizon,
"weakest_prefix_signal": candidate.weakest_prefix_signal,
"expected_reads": candidate.expected_reads,
"expected_net_savings": expected_net_savings,
"candidate_kind": candidate.kind.label(),
}),
);
previous_cumulative_tokens = candidate.cumulative_tokens;
}
}
fn emit_plan_limit_reached_if_needed(
plan: &EconomicsPlan,
candidates: &[BreakpointCandidate],
max_breakpoints: usize,
capabilities: &ModelFamilyCapabilities,
) {
if plan.planned_breakpoints.len() >= max_breakpoints && candidates.len() > max_breakpoints {
acg_debug::emit(
"economics_plan_limit_reached",
json!({
"model_family": capabilities.model_family,
"max_breakpoints": max_breakpoints,
"planned_breakpoints": plan.planned_breakpoints.len(),
}),
);
}
}
fn build_prefix_stats(
prompt_ir: &PromptIR,
stability: &StabilityAnalysisResult,
stable_prefix_end: usize,
minimum_cacheable_tokens: u32,
observed_reuse_horizon: f64,
capabilities: &ModelFamilyCapabilities,
observation_count: u32,
) -> Vec<PrefixStats> {
let mut stats = Vec::with_capacity(stable_prefix_end);
let mut cumulative_tokens = 0_u32;
let mut weakest_prefix_signal = 1.0_f64;
for (index, score) in stability.scores.iter().take(stable_prefix_end).enumerate() {
if score.classification != StabilityClass::Stable {
acg_debug::emit(
"economics_candidate_rejected",
json!({
"reason": "non_stable_block",
"model_family": capabilities.model_family,
"block_index": index,
"classification": format!("{:?}", score.classification),
"score": score.score,
"confidence": score.confidence,
"observation_count": observation_count,
}),
);
break;
}
cumulative_tokens =
cumulative_tokens.saturating_add(estimate_block_tokens(&prompt_ir.blocks[index]));
weakest_prefix_signal =
weakest_prefix_signal.min(prefix_signal(score.score, score.confidence));
if cumulative_tokens < minimum_cacheable_tokens {
acg_debug::emit(
"economics_candidate_rejected",
json!({
"reason": "below_min_cacheable_tokens",
"model_family": capabilities.model_family,
"block_index": index,
"cumulative_tokens": cumulative_tokens,
"minimum_cacheable_tokens": minimum_cacheable_tokens,
"score": score.score,
"confidence": score.confidence,
"weakest_prefix_signal": weakest_prefix_signal,
"observed_reuse_horizon": observed_reuse_horizon,
}),
);
}
stats.push(PrefixStats {
cumulative_tokens,
weakest_prefix_signal,
});
}
stats
}
fn collect_breakpoint_candidates(
prompt_ir: &PromptIR,
prefix_stats: &[PrefixStats],
stable_prefix_end: usize,
observed_reuse_horizon: f64,
minimum_cacheable_tokens: u32,
capabilities: &ModelFamilyCapabilities,
) -> Vec<BreakpointCandidate> {
let mut candidates = Vec::new();
let mut previous_cumulative_tokens = 0_u32;
let scan_end = prefix_stats.len().min(stable_prefix_end);
for (index, stats) in prefix_stats.iter().take(scan_end).enumerate() {
let stats = *stats;
if stats.cumulative_tokens < minimum_cacheable_tokens {
previous_cumulative_tokens = stats.cumulative_tokens;
continue;
}
let threshold_crossing = previous_cumulative_tokens < minimum_cacheable_tokens
&& stats.cumulative_tokens >= minimum_cacheable_tokens;
let kind =
classify_candidate_boundary(prompt_ir, index, stable_prefix_end, threshold_crossing);
if let Some(kind) = kind {
let candidate = BreakpointCandidate {
stable_prefix_end: index + 1,
cumulative_tokens: stats.cumulative_tokens,
weakest_prefix_signal: stats.weakest_prefix_signal,
expected_reads: observed_reuse_horizon * stats.weakest_prefix_signal,
kind,
};
acg_debug::emit(
"economics_candidate_generated",
json!({
"model_family": capabilities.model_family,
"block_index": index,
"stable_prefix_end": candidate.stable_prefix_end,
"cumulative_tokens": candidate.cumulative_tokens,
"expected_reads": candidate.expected_reads,
"weakest_prefix_signal": candidate.weakest_prefix_signal,
"candidate_kind": candidate.kind.label(),
}),
);
candidates.push(candidate);
} else {
acg_debug::emit(
"economics_candidate_rejected",
json!({
"reason": "non_semantic_internal_boundary",
"model_family": capabilities.model_family,
"block_index": index,
"cumulative_tokens": stats.cumulative_tokens,
}),
);
}
previous_cumulative_tokens = stats.cumulative_tokens;
}
candidates
}
fn classify_candidate_boundary(
prompt_ir: &PromptIR,
index: usize,
stable_prefix_end: usize,
threshold_crossing: bool,
) -> Option<CandidateKind> {
let block = prompt_ir.blocks.get(index)?;
let next = prompt_ir.blocks.get(index + 1);
let next_is_tool_schema =
next.is_some_and(|candidate| candidate.content_type == BlockContentType::ToolSchema);
if block.content_type == BlockContentType::ToolSchema
&& next_is_tool_schema
&& !threshold_crossing
{
return None;
}
let kind = match (block.role, block.content_type) {
(_, BlockContentType::ToolSchema) => CandidateKind::ToolCluster,
_ if block.provenance == ProvenanceLabel::Retrieval => CandidateKind::Retrieval,
(PromptRole::User, _) => CandidateKind::User,
(
_,
BlockContentType::StructuredOutput
| BlockContentType::ToolResult
| BlockContentType::Image,
) => CandidateKind::Structured,
(PromptRole::System, _) => CandidateKind::System,
_ if index + 1 >= stable_prefix_end => CandidateKind::Generic,
_ => CandidateKind::Generic,
};
Some(kind)
}
fn select_breakpoint_candidates(
candidates: &[BreakpointCandidate],
max_breakpoints: usize,
pricing: &CacheEconomics,
) -> Option<Vec<usize>> {
if candidates.is_empty() || max_breakpoints == 0 {
return None;
}
let mut dp = vec![vec![None; max_breakpoints + 1]; candidates.len()];
populate_candidate_selection_dp(&mut dp, candidates, max_breakpoints, pricing);
let (candidate_index, breakpoint_count, _) = best_terminal_candidate(&dp, candidates)?;
Some(reconstruct_selected_candidates(
&dp,
candidate_index,
breakpoint_count,
))
}
fn populate_candidate_selection_dp(
dp: &mut [Vec<Option<CandidateSelectionState>>],
candidates: &[BreakpointCandidate],
max_breakpoints: usize,
pricing: &CacheEconomics,
) {
for (candidate_index, candidate) in candidates.iter().enumerate() {
seed_candidate_selection_state(dp, candidate_index, candidate, pricing);
extend_candidate_selection_state(dp, candidate_index, candidates, max_breakpoints, pricing);
}
}
fn seed_candidate_selection_state(
dp: &mut [Vec<Option<CandidateSelectionState>>],
candidate_index: usize,
candidate: &BreakpointCandidate,
pricing: &CacheEconomics,
) {
let base_value =
pricing.marginal_net_savings(candidate.cumulative_tokens, candidate.expected_reads);
if base_value > 0.0 {
dp[candidate_index][1] = Some(CandidateSelectionState {
total_value: base_value,
previous_candidate_index: None,
});
}
}
fn extend_candidate_selection_state(
dp: &mut [Vec<Option<CandidateSelectionState>>],
candidate_index: usize,
candidates: &[BreakpointCandidate],
max_breakpoints: usize,
pricing: &CacheEconomics,
) {
for breakpoint_count in 2..=max_breakpoints.min(candidate_index + 1) {
for previous_index in 0..candidate_index {
let Some(proposal) = candidate_extension_proposal(
dp,
candidates,
candidate_index,
breakpoint_count,
previous_index,
pricing,
) else {
continue;
};
if is_better_candidate_state(
dp[candidate_index][breakpoint_count],
proposal,
candidates,
candidate_index,
) {
dp[candidate_index][breakpoint_count] = Some(proposal);
}
}
}
}
fn candidate_extension_proposal(
dp: &[Vec<Option<CandidateSelectionState>>],
candidates: &[BreakpointCandidate],
candidate_index: usize,
breakpoint_count: usize,
previous_index: usize,
pricing: &CacheEconomics,
) -> Option<CandidateSelectionState> {
let previous_state = dp[previous_index][breakpoint_count - 1]?;
let marginal_tokens = candidates[candidate_index]
.cumulative_tokens
.saturating_sub(candidates[previous_index].cumulative_tokens);
if marginal_tokens == 0 {
return None;
}
let segment_value =
pricing.marginal_net_savings(marginal_tokens, candidates[candidate_index].expected_reads);
if segment_value <= 0.0 {
return None;
}
Some(CandidateSelectionState {
total_value: previous_state.total_value + segment_value,
previous_candidate_index: Some(previous_index),
})
}
fn best_terminal_candidate(
dp: &[Vec<Option<CandidateSelectionState>>],
candidates: &[BreakpointCandidate],
) -> Option<(usize, usize, CandidateSelectionState)> {
let mut best_terminal: Option<(usize, usize, CandidateSelectionState)> = None;
for (candidate_index, row) in dp.iter().enumerate() {
for (breakpoint_count, state) in row.iter().enumerate().skip(1) {
let Some(state) = state else {
continue;
};
if is_better_terminal_state(
best_terminal,
(candidate_index, breakpoint_count, *state),
candidates,
) {
best_terminal = Some((candidate_index, breakpoint_count, *state));
}
}
}
best_terminal
}
fn reconstruct_selected_candidates(
dp: &[Vec<Option<CandidateSelectionState>>],
mut candidate_index: usize,
mut breakpoint_count: usize,
) -> Vec<usize> {
let mut selected = Vec::new();
while breakpoint_count > 0 {
selected.push(candidate_index);
let state = dp[candidate_index][breakpoint_count]
.expect("terminal DP state must be present during reconstruction");
if let Some(previous_index) = state.previous_candidate_index {
candidate_index = previous_index;
breakpoint_count -= 1;
} else {
break;
}
}
selected.reverse();
selected
}
fn is_better_candidate_state(
current: Option<CandidateSelectionState>,
proposal: CandidateSelectionState,
_candidates: &[BreakpointCandidate],
_proposal_index: usize,
) -> bool {
let Some(current) = current else {
return true;
};
if proposal.total_value > current.total_value + 1e-9 {
return true;
}
if current.total_value > proposal.total_value + 1e-9 {
return false;
}
proposal.previous_candidate_index > current.previous_candidate_index
}
fn is_better_terminal_state(
current: Option<(usize, usize, CandidateSelectionState)>,
proposal: (usize, usize, CandidateSelectionState),
candidates: &[BreakpointCandidate],
) -> bool {
let Some((current_index, current_breakpoints, current_state)) = current else {
return true;
};
let (proposal_index, proposal_breakpoints, proposal_state) = proposal;
if proposal_state.total_value > current_state.total_value + 1e-9 {
return true;
}
if current_state.total_value > proposal_state.total_value + 1e-9 {
return false;
}
if proposal_breakpoints < current_breakpoints {
return true;
}
if proposal_breakpoints > current_breakpoints {
return false;
}
if candidates[proposal_index].kind.priority() > candidates[current_index].kind.priority() {
return true;
}
if candidates[proposal_index].kind.priority() < candidates[current_index].kind.priority() {
return false;
}
candidates[proposal_index].stable_prefix_end > candidates[current_index].stable_prefix_end
}
fn estimate_block_tokens(block: &PromptBlock) -> u32 {
block.token_metadata.as_ref().map_or_else(
|| {
let estimated = block.content.len() / 4;
estimated.try_into().unwrap_or(u32::MAX)
},
|metadata| metadata.token_count,
)
}
fn prefix_signal(score: f64, confidence: f64) -> f64 {
score.clamp(0.0, 1.0) * confidence.clamp(0.0, 1.0)
}
#[cfg(test)]
#[path = "../../tests/unit/acg/economics_internal_tests.rs"]
mod tests;