Skip to main content

hematite/agent/
economics.rs

1// ── Session Economics Tracking ───────────────────────────────────────────────
2
3use serde::Serialize;
4
5// ── Per-turn context budget ledger ────────────────────────────────────────────
6
7/// Token cost of a single tool result within a turn.
8#[derive(Debug, Clone)]
9pub struct ToolCost {
10    pub name: String,
11    /// Estimated tokens (result_chars / 4).
12    pub tokens: usize,
13}
14
15/// Per-turn breakdown of context consumed.
16/// Populated at turn end and surfaced in the SPECULAR panel.
17#[derive(Debug, Clone)]
18pub struct TurnBudget {
19    /// Actual input tokens charged this turn (precise — from API usage delta).
20    pub input_tokens: usize,
21    /// Actual output tokens generated this turn (precise — from API usage delta).
22    pub output_tokens: usize,
23    /// Estimated prior-history tokens (chars / 4) — context already present before this turn.
24    pub history_est: usize,
25    /// Per-tool result costs (estimated tokens from result length).
26    pub tool_costs: Vec<ToolCost>,
27    /// Context window fill percentage at turn end.
28    pub context_pct: u8,
29}
30
31impl TurnBudget {
32    /// Compact ledger string for the SPECULAR panel and /budget command.
33    pub fn render(&self) -> String {
34        let total = self.input_tokens + self.output_tokens;
35        let mut parts = Vec::with_capacity(self.tool_costs.len() + 2);
36
37        if self.history_est > 0 {
38            parts.push(format!("prior hist ~{}t", self.history_est));
39        }
40        for tc in &self.tool_costs {
41            parts.push(format!("{} ~{}t", tc.name, tc.tokens));
42        }
43        if self.output_tokens > 0 {
44            parts.push(format!("model out {}t", self.output_tokens));
45        }
46
47        let breakdown = if parts.is_empty() {
48            String::new()
49        } else {
50            format!("\n  {}", parts.join("  |  "))
51        };
52
53        format!(
54            "Context budget: +{}t this turn  ({}% ctx){}\n  \
55             Tip: large tool results are the most common cause of context pressure.",
56            total, self.context_pct, breakdown
57        )
58    }
59}
60
61/// Tracks token usage and tool calls for a session.
62#[derive(Default)]
63pub struct SessionEconomics {
64    /// Input tokens accumulated across all calls.
65    pub input_tokens: usize,
66    /// Output tokens accumulated across all calls.
67    pub output_tokens: usize,
68    /// List of tool calls with name and success/fail status.
69    pub tools_used: Vec<ToolRecord>,
70}
71
72impl SessionEconomics {
73    /// Create a new empty economics tracker.
74    pub fn new() -> Self {
75        Self {
76            input_tokens: 0,
77            output_tokens: 0,
78            tools_used: Vec::new(),
79        }
80    }
81
82    /// Record a tool call.
83    pub fn record_tool(&mut self, name: &str, success: bool) {
84        self.tools_used.push(ToolRecord {
85            name: name.to_string(),
86            success,
87        });
88    }
89}
90
91/// A record of a tool call.
92#[derive(Serialize, Clone, Debug)]
93pub struct ToolRecord {
94    pub name: String,
95    pub success: bool,
96}
97
98// ── Pricing constants ─────────────────────────────────────────────────────────
99
100/// Input token price: $0.002 per 1K tokens.
101pub const INPUT_PRICE_PER_1K: f64 = 0.002;
102
103/// Output token price: $0.006 per 1K tokens.
104pub const OUTPUT_PRICE_PER_1K: f64 = 0.006;
105
106// ── Report generation ────────────────────────────────────────────────────────
107
108impl SessionEconomics {
109    /// Calculate simulated cost based on token usage.
110    pub fn simulated_cost(&self) -> f64 {
111        let input_cost = (self.input_tokens as f64 / 1000.0) * INPUT_PRICE_PER_1K;
112        let output_cost = (self.output_tokens as f64 / 1000.0) * OUTPUT_PRICE_PER_1K;
113        input_cost + output_cost
114    }
115
116    /// Generate a JSON report of the session economics.
117    pub fn to_json(&self) -> String {
118        use serde_json::json;
119        json!({
120            "session_economics": {
121                "input_tokens": self.input_tokens,
122                "output_tokens": self.output_tokens,
123                "total_tokens": self.input_tokens + self.output_tokens,
124                "tools_used": self.tools_used.iter().map(|t| {
125                    json!({
126                        "name": t.name,
127                        "success": t.success
128                    })
129                }).collect::<Vec<_>>(),
130                "simulated_cost_usd": self.simulated_cost()
131            }
132        })
133        .to_string()
134    }
135}