use lash_core::PreparedContext;
use lash_core::plugin::{HistoryError, TurnContextTransform, TurnTransformContext};
use crate::driver::SharedPromptUsage;
pub(super) const BUDGET_WARNING_STATUS: &str = "rlm_context_budget_warning";
pub(super) struct BudgetUsageObserver {
pub(super) cell: SharedPromptUsage,
}
#[async_trait::async_trait]
impl TurnContextTransform for BudgetUsageObserver {
fn id(&self) -> &'static str {
"rlm.budget_usage_observer"
}
async fn transform(
&self,
ctx: &TurnTransformContext<'_>,
input: PreparedContext,
) -> Result<PreparedContext, HistoryError> {
if let Ok(mut guard) = self.cell.write() {
*guard = ctx.prompt_usage.clone();
}
Ok(input)
}
}
#[cfg(test)]
mod tests {
use crate::rlm_support::format_budget_suffix;
fn prompt_usage(context_budget_tokens: usize) -> lash_core::PromptUsage {
lash_core::PromptUsage {
prompt_context_tokens: context_budget_tokens,
input_tokens: context_budget_tokens,
cached_input_tokens: 0,
context_budget_tokens,
}
}
#[test]
fn budget_prompt_contribution_below_advisory_floor_emits_status_only() {
let usage = prompt_usage(47_213);
let content = format_budget_suffix(0, Some(&usage), Some(200_000))
.expect("budget suffix should render");
assert!(content.contains("Tokens: 47213 · frame switch threshold: 200000 (23%)"));
assert!(content.contains("Turn:"));
assert!(!content.contains("Look for a clean frame switch point"));
assert!(!content.contains("Budget tight"));
assert!(!content.contains("Past the frame switch threshold"));
}
#[test]
fn budget_prompt_contribution_advisory_tier_60_to_89_pct() {
let usage = prompt_usage(75_000);
let content = format_budget_suffix(0, Some(&usage), Some(100_000))
.expect("budget suffix should render");
assert!(content.contains("Tokens: 75000 · frame switch threshold: 100000 (75%)"));
assert!(content.contains("Look for a clean frame switch point"));
assert!(!content.contains("Budget tight"));
assert!(!content.contains("Past the frame switch threshold"));
}
#[test]
fn budget_prompt_contribution_tight_tier_90_to_99_pct() {
let usage = prompt_usage(95_000);
let content = format_budget_suffix(0, Some(&usage), Some(100_000))
.expect("budget suffix should render");
assert!(content.contains("Tokens: 95000 · frame switch threshold: 100000 (95%)"));
assert!(content.contains("Budget tight"));
assert!(content.contains("`control.continue_as(...)`"));
assert!(!content.contains("Past the frame switch threshold"));
assert!(!content.contains("Look for a clean frame switch point"));
}
#[test]
fn budget_prompt_contribution_over_threshold_forces_frame_switch() {
let usage = prompt_usage(120_292);
let content = format_budget_suffix(0, Some(&usage), Some(100_000))
.expect("budget suffix should render");
assert!(content.contains("Tokens: 120292 · frame switch threshold: 100000 (120%)"));
assert!(content.contains("Past the frame switch threshold"));
assert!(content.contains("End this block with `control.continue_as(...)` now"));
assert!(content.contains("do not call `submit`"));
assert!(content.contains("`task` + `seed`"));
}
#[test]
fn budget_prompt_contribution_omits_without_configured_budget() {
let usage = prompt_usage(47_213);
assert!(format_budget_suffix(0, Some(&usage), None).is_none());
}
#[test]
fn budget_prompt_contribution_omits_without_used_tokens() {
let usage = prompt_usage(0);
assert!(format_budget_suffix(0, Some(&usage), Some(200_000)).is_none());
}
}