agentmux_srv/ambient/spend.rs
1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Tells the frontend what every ambient call spent, for the status bar's token
5//! totals.
6//!
7//! Each call's tokens are published once, from the one place every call ends
8//! (`call::Slot::run`), as an `ambient:spent` event the frontend records
9//! (`frontend/app/store/ambient-spend.ts`). Before, only the calls a pane itself
10//! asked for were counted, by whichever pane got the reply, and five purposes
11//! that run in the background (recovered titles, workflow names, previews,
12//! narration, running summaries) were never counted at all.
13//! docs/reports/REPORT_AMBIENT_FRAMEWORK_REASSESSMENT_2026_10_08.md section 5.8.
14
15use std::sync::{Arc, OnceLock};
16
17use crate::agents::TokenCounts;
18use crate::backend::eventbus::EventBus;
19
20/// The event a purpose's spend is published as.
21pub const EVENT_AMBIENT_SPENT: &str = "ambient:spent";
22
23static BUS: OnceLock<Arc<EventBus>> = OnceLock::new();
24
25/// Publish spend on `bus` from now on. Set once at startup; without it (tests,
26/// tools) spend is not published.
27pub fn publish_on(bus: Arc<EventBus>) {
28 let _ = BUS.set(bus);
29}
30
31/// `ambient:spent`: what one ambient model call spent, and on what.
32#[derive(Debug, Clone, serde::Serialize, ts_rs::TS)]
33#[ts(export, export_to = "../../../frontend/types/rpc/")]
34pub struct AmbientSpentEvent {
35 pub purpose: String,
36 pub tokens: TokenCounts,
37}
38
39/// Publish what one call for `purpose` spent.
40pub fn report(purpose: &str, tokens: &TokenCounts) {
41 let Some(bus) = BUS.get() else { return };
42 bus.broadcast_typed::<crate::backend::mps_events::AmbientSpent>(&AmbientSpentEvent { purpose: purpose.to_string(), tokens: tokens.clone() });
43}