use chrono::Utc;
use serde_json::{json, Value};
use std::fs;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
fn env_str(key: &str, default: &str) -> String {
std::env::var(key).unwrap_or_else(|_| default.to_string())
}
fn env_u64(key: &str, default: u64) -> u64 {
std::env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
}
enum CircuitStatus {
Closed,
HalfOpen,
Open(u64),
}
pub fn cmd_token_budget(tool: Option<String>) -> i32 {
if std::env::var("YANA_BUDGET_BYPASS").ok().as_deref() == Some("1") {
println!("[token-budget-guard] BYPASS active");
return 0;
}
let budget_path = env_str("YANA_TOKEN_BUDGET", "core/memory/L2_session/token-budget.json");
let circuit_path = env_str("YANA_CIRCUIT_STATE", "core/memory/L2_session/circuit-state.json");
let max_loop_tokens = env_u64("YANA_MAX_LOOP_TOKENS", 50_000);
let max_attempts = env_u64("YANA_MAX_FIX_ATTEMPTS", 5);
let cooldown_seconds = env_u64("YANA_CIRCUIT_COOLDOWN", 60);
let log_file = env_str("YANA_LOG", "/tmp/yana-ai-audit.log");
let fast_tier_model = env_str("YANA_FAST_TIER_MODEL", "claude-haiku-4-5-20251001");
let tool_name = tool
.or_else(|| std::env::var("CLAUDE_TOOL_NAME").ok())
.unwrap_or_else(|| "unknown".to_string());
let timestamp = Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
let now_epoch = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
let mut budget = load_or_init(&budget_path, || {
json!({
"session_start": timestamp,
"total_tokens_used": 0,
"actions": [],
"loop_attempts": {},
"fast_tier_triggered": false,
})
});
let mut circuits = load_or_init(&circuit_path, || json!({ "circuits": {} }));
let status = circuit_status_for(&circuits, &tool_name, now_epoch, cooldown_seconds);
if let CircuitStatus::Open(remaining) = status {
print_open_box(&tool_name, remaining, &fast_tier_model);
append_log(&log_file, &format!(
"[{timestamp}] CIRCUIT-OPEN tool='{tool_name}' cooldown_remaining={remaining}s"
));
return 1;
}
let total_tokens = budget.get("total_tokens_used").and_then(Value::as_u64).unwrap_or(0);
let loop_count = budget
.get("loop_attempts")
.and_then(|v| v.get(&tool_name))
.and_then(Value::as_u64)
.unwrap_or(0);
if loop_count >= max_attempts {
print_trigger_box(&tool_name, loop_count, max_attempts, total_tokens, cooldown_seconds, &fast_tier_model);
let prev_open_count = circuits
.get("circuits")
.and_then(|c| c.get(&tool_name))
.and_then(|e| e.get("open_count"))
.and_then(Value::as_u64)
.unwrap_or(0);
let open_count = prev_open_count + 1;
let stored_cooldown = match open_count {
1 => 60,
2 => 300,
_ => 1800,
};
ensure_object(&mut circuits, "circuits");
circuits["circuits"][tool_name.as_str()] = json!({
"state": "open",
"opened_at": timestamp,
"opened_at_epoch": now_epoch,
"open_count": open_count,
"cooldown_seconds": stored_cooldown,
"reason": format!("Loop: {tool_name} called >={max_attempts} times without success"),
});
write_json(&circuit_path, &circuits);
budget["fast_tier_triggered"] = json!(true);
budget["fast_tier_tool"] = json!(tool_name);
write_json(&budget_path, &budget);
append_log(&log_file, &format!(
"[{timestamp}] CIRCUIT-TRIGGERED tool='{tool_name}' loop_count={loop_count} tokens={total_tokens}"
));
return 1; }
if total_tokens > max_loop_tokens {
println!("[token-budget-guard] BUDGET WARNING: {total_tokens} tokens used (limit: {max_loop_tokens})");
println!("[token-budget-guard] Run /cost-report to review ROI before continuing");
}
if matches!(status, CircuitStatus::HalfOpen) {
if let Some(entry) = circuits.get_mut("circuits").and_then(|c| c.get_mut(&tool_name)) {
entry["state"] = json!("closed");
entry["closed_at"] = json!(Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true));
write_json(&circuit_path, &circuits);
}
println!("[token-budget-guard] Circuit CLOSED for {tool_name} — probe succeeded");
}
ensure_object(&mut budget, "loop_attempts");
let new_count = budget["loop_attempts"].get(tool_name.as_str()).and_then(Value::as_u64).unwrap_or(0) + 1;
budget["loop_attempts"][tool_name.as_str()] = json!(new_count);
write_json(&budget_path, &budget);
println!("[token-budget-guard] OK — {tool_name} (attempt {} / {max_attempts})", loop_count + 1);
0
}
fn circuit_status_for(circuits: &Value, tool: &str, now_epoch: u64, cooldown_seconds: u64) -> CircuitStatus {
let info = circuits.get("circuits").and_then(|c| c.get(tool));
let state = info.and_then(|i| i.get("state")).and_then(Value::as_str).unwrap_or("closed");
match state {
"open" => {
let opened_at_epoch = info.and_then(|i| i.get("opened_at_epoch")).and_then(Value::as_u64).unwrap_or(0);
let elapsed = now_epoch.saturating_sub(opened_at_epoch);
if elapsed >= cooldown_seconds {
CircuitStatus::HalfOpen
} else {
CircuitStatus::Open(cooldown_seconds - elapsed)
}
}
"half-open" => CircuitStatus::HalfOpen,
_ => CircuitStatus::Closed,
}
}
fn ensure_object(parent: &mut Value, key: &str) {
if !parent.get(key).is_some_and(Value::is_object) {
parent[key] = json!({});
}
}
fn load_or_init(path: &str, default: impl Fn() -> Value) -> Value {
if let Ok(raw) = fs::read_to_string(path) {
if let Ok(parsed) = serde_json::from_str::<Value>(&raw) {
return parsed;
}
}
let value = default();
write_json(path, &value);
value
}
fn write_json(path: &str, value: &Value) {
if let Some(parent) = Path::new(path).parent() {
let _ = fs::create_dir_all(parent);
}
let _ = fs::write(path, serde_json::to_string_pretty(value).unwrap_or_default());
}
fn append_log(log_file: &str, line: &str) {
use std::io::Write;
if let Ok(mut f) = fs::OpenOptions::new().create(true).append(true).open(log_file) {
let _ = writeln!(f, "{line}");
}
}
fn print_open_box(tool: &str, remaining: u64, fast_tier_model: &str) {
println!("╔══════════════════════════════════════════════════════╗");
println!("║ [token-budget-guard] CIRCUIT BREAKER — OPEN ║");
println!("╚══════════════════════════════════════════════════════╝");
println!(" Tool : {tool}");
println!(" State : OPEN (cooldown: {remaining}s remaining)");
println!(" Action : HARD BLOCKED — loop detected, circuit is open");
println!(" Fix : Wait for cooldown, then retry with a different strategy");
println!(" Fast tier: Switch model to {fast_tier_model} to reduce cost");
}
fn print_trigger_box(tool: &str, loop_count: u64, max_attempts: u64, tokens: u64, cooldown_seconds: u64, fast_tier_model: &str) {
println!("╔══════════════════════════════════════════════════════╗");
println!("║ [token-budget-guard] CIRCUIT BREAKER TRIGGERED ║");
println!("╚══════════════════════════════════════════════════════╝");
println!(" Tool : {tool}");
println!(" Loop count : {loop_count} / {max_attempts} (threshold exceeded)");
println!(" Tokens used: {tokens}");
println!(" Action : Circuit OPENED — tool BLOCKED for {cooldown_seconds}s");
println!();
println!(" ── Fast-Tier Recommendation ──────────────────────────");
println!(" Switch model to: {fast_tier_model}");
println!(" Reason: Sonnet costs accumulating on a stuck loop.");
println!(" Command: Set ANTHROPIC_MODEL={fast_tier_model} in your env");
println!();
println!(" ── Recovery Options ──────────────────────────────────");
println!(" 1. Stop the loop — pick a completely different approach");
println!(" 2. Use /tree-of-thoughts to re-plan from scratch");
println!(" 3. Escalate to human: too complex for auto-fix");
println!();
}