1use crate::outline::estimate_tokens;
4use serde::{Deserialize, Serialize};
5use std::{borrow::Cow, collections::BTreeMap};
6use thiserror::Error;
7
8pub const TOKEN_ESTIMATE_KIND: &str = "heuristic";
10pub const TOKEN_ESTIMATOR: &str = "chars_or_bytes_div_ceil_4";
12pub const TOKEN_ESTIMATE_SCOPE: &str = "workflow_payload_estimate_not_model_billing_tokens";
14pub const TOKEN_PROVIDER_HEURISTIC: &str = "heuristic";
16pub const TOKEN_MODEL_UNKNOWN: &str = "unknown";
18pub const TOKENIZER_BACKEND_HEURISTIC: &str = "chars_div_4";
20pub const TOKEN_ACCURACY_HEURISTIC: &str = "heuristic_estimate";
22pub const TOKEN_BUCKET_FULL_FILE_COMPRESSION: &str = "full_file_compression";
24pub const TOKEN_BUCKET_NAVIGATION_AVOIDANCE: &str = "navigation_avoidance";
26pub const TOKEN_BASELINE_FULL_FILE: &str = "full_file";
28pub const TOKEN_BASELINE_SELECTED_CANDIDATES: &str = "selected_candidates";
30pub const TOKEN_BASELINE_DIRECTORY_WALK: &str = "directory_walk";
32pub const TOKEN_CONFIDENCE_OBSERVED: &str = "observed";
34pub const TOKEN_CONFIDENCE_INFERRED: &str = "inferred";
36pub const TOKEN_CONFIDENCE_POLICY_ESTIMATE: &str = "policy_estimate";
38pub const TOKEN_TRACE_HEURISTIC: &str = "heuristic=ceil(chars_or_bytes/4)";
40pub const TOKEN_ACCOUNTING_OBSERVED_DELTA: &str = "observed_delta";
42pub const TOKEN_ACCOUNTING_MODELED_AVOIDANCE: &str = "modeled_avoidance";
44pub const TOKEN_ESTIMATE_METHOD_HEURISTIC: &str = "heuristic_chars_or_bytes_div_ceil_4";
46pub const TOKEN_DEDUPE_SCOPE_EVENT: &str = "event";
48pub const TOKEN_DEDUPE_SCOPE_SESSION: &str = "session";
50pub const READ_AVOIDANCE_CONFIDENCE_OBSERVED: &str = "observed";
52pub const READ_AVOIDANCE_CONFIDENCE_MODELED: &str = "modeled";
54pub const READ_AVOIDANCE_CONFIDENCE_NOT_RECORDED: &str = "not_recorded";
56pub const READ_AVOIDANCE_SCOPE: &str =
58 "summary_search_slice_calls_that_likely_replaced_whole_file_reads";
59pub const TOKEN_COMMAND_SUMMARY: &str = "summary";
61pub const TOKEN_COMMAND_OUTLINE: &str = "outline";
63pub const TOKEN_COMMAND_SLICE: &str = "slice";
65pub const TOKEN_COMMAND_SYMBOL_SLICE: &str = "symbol-slice";
67pub const TOKEN_COMMAND_SEARCH: &str = "search";
69pub const TOKEN_COMMAND_MCP_FILE_SUMMARY: &str = "mcp.atlas_file_summary";
71pub const TOKEN_COMMAND_MCP_OUTLINE: &str = "mcp.atlas_outline";
73pub const TOKEN_COMMAND_MCP_SLICE: &str = "mcp.atlas_slice";
75pub const TOKEN_COMMAND_MCP_SEARCH: &str = "mcp.atlas_search";
77
78#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
80pub enum TelemetryContractError {
81 #[error("the zero usage instance identifier is reserved")]
83 ZeroUsageInstanceId,
84}
85
86#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
88pub struct UsageInstanceId([u8; 16]);
89
90impl UsageInstanceId {
91 pub fn from_bytes(bytes: [u8; 16]) -> Result<Self, TelemetryContractError> {
97 if bytes == [0; 16] {
98 return Err(TelemetryContractError::ZeroUsageInstanceId);
99 }
100 Ok(Self(bytes))
101 }
102
103 #[must_use]
105 pub const fn as_bytes(self) -> [u8; 16] {
106 self.0
107 }
108}
109
110#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
112#[serde(rename_all = "snake_case")]
113pub enum UsageInstanceOwner {
114 CliInvocation,
116 McpProcess,
118 LibraryHandle,
120 MigratedLegacy,
122}
123
124impl UsageInstanceOwner {
125 #[must_use]
127 pub const fn as_str(self) -> &'static str {
128 match self {
129 Self::CliInvocation => "cli_invocation",
130 Self::McpProcess => "mcp_process",
131 Self::LibraryHandle => "library_handle",
132 Self::MigratedLegacy => "migrated_legacy",
133 }
134 }
135
136 #[must_use]
138 pub fn parse(value: &str) -> Option<Self> {
139 match value {
140 "cli_invocation" => Some(Self::CliInvocation),
141 "mcp_process" => Some(Self::McpProcess),
142 "library_handle" => Some(Self::LibraryHandle),
143 "migrated_legacy" => Some(Self::MigratedLegacy),
144 _ => None,
145 }
146 }
147}
148
149#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
151#[serde(rename_all = "snake_case")]
152pub enum UsageDetailAvailability {
153 Retained,
155 Partial,
157 Expired,
159 #[default]
161 Unavailable,
162}
163
164impl UsageDetailAvailability {
165 #[must_use]
167 pub const fn as_str(self) -> &'static str {
168 match self {
169 Self::Retained => "retained",
170 Self::Partial => "partial",
171 Self::Expired => "expired",
172 Self::Unavailable => "unavailable",
173 }
174 }
175
176 #[must_use]
178 pub fn parse(value: &str) -> Option<Self> {
179 match value {
180 "retained" => Some(Self::Retained),
181 "partial" => Some(Self::Partial),
182 "expired" => Some(Self::Expired),
183 "unavailable" => Some(Self::Unavailable),
184 _ => None,
185 }
186 }
187}
188
189#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
191pub struct TokenAccountingTotals {
192 pub measured_tokens_saved: i128,
194 pub gross_modeled_tokens_avoided: i128,
196 pub deduped_modeled_tokens_avoided: i128,
198 pub repeated_baselines_deduped: u128,
200 pub observed_file_read_replacements: u128,
202 pub modeled_file_reads_avoided: u128,
204}
205
206#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
208pub struct UsageEvent {
209 pub session_id: String,
211 pub command: String,
213 pub path: Option<String>,
215 pub query: Option<String>,
217 pub estimated_tokens_without_projectatlas: Option<usize>,
219 pub estimated_tokens_with_projectatlas: Option<usize>,
221 pub estimated_tokens_saved: Option<isize>,
223 #[serde(default = "default_token_savings_bucket")]
225 pub token_savings_bucket: String,
226 #[serde(default = "default_token_provider")]
228 pub provider: String,
229 #[serde(default = "default_token_model")]
231 pub model: String,
232 #[serde(default = "default_tokenizer_backend")]
234 pub tokenizer_backend: String,
235 #[serde(default = "default_token_accuracy")]
237 pub accuracy: String,
238 #[serde(default = "default_token_baseline_kind")]
240 pub baseline_kind: String,
241 #[serde(default = "default_token_confidence")]
243 pub confidence: String,
244 #[serde(default = "default_token_trace")]
246 pub calculation_trace: String,
247 #[serde(default = "default_accounting_layer")]
249 pub accounting_layer: String,
250 #[serde(default = "default_estimate_method")]
252 pub estimate_method: String,
253 #[serde(default = "default_denominator_kind")]
255 pub denominator_kind: String,
256 #[serde(default)]
258 pub baseline_identity: String,
259 #[serde(default)]
261 pub baseline_fingerprint: String,
262 #[serde(default = "default_dedupe_scope")]
264 pub dedupe_scope: String,
265}
266
267#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
269pub struct TokenBucketOverview {
270 pub token_savings_bucket: String,
272 pub provider: String,
274 pub model: String,
276 pub tokenizer_backend: String,
278 pub accuracy: String,
280 pub baseline_kind: String,
282 pub confidence: String,
284 pub calls: usize,
286 pub estimated_without_projectatlas: usize,
288 pub estimated_with_projectatlas: usize,
290 pub estimated_saved: isize,
292 pub savings_rate: Option<f64>,
294 pub accounting_layer: String,
296 pub estimate_method: String,
298 pub denominator_kind: String,
300 pub dedupe_scope: String,
302}
303
304#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
306pub struct TokenCalibrationOverview {
307 pub tokenizer: String,
309 pub provider: String,
311 pub model: String,
313 pub tokenizer_backend: String,
315 pub accuracy: String,
317 pub files: usize,
319 pub bytes: usize,
321 pub heuristic_tokens: usize,
323 pub calibrated_tokens: usize,
325 pub heuristic_to_calibrated_ratio: Option<f64>,
327}
328
329#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
331#[serde(rename_all = "snake_case")]
332pub enum AgentEfficiencyEvidenceState {
333 #[default]
335 Unavailable,
336 Failed,
338 Incompatible,
340 Partial,
342 Compatible,
344}
345
346impl AgentEfficiencyEvidenceState {
347 #[must_use]
349 pub const fn as_str(self) -> &'static str {
350 match self {
351 Self::Unavailable => "unavailable",
352 Self::Failed => "failed",
353 Self::Incompatible => "incompatible",
354 Self::Partial => "partial",
355 Self::Compatible => "compatible",
356 }
357 }
358}
359
360#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
362#[serde(rename_all = "snake_case")]
363pub enum AgentEfficiencyBaseline {
364 FrozenProjectAtlasV0326,
366 PlainCodex,
368}
369
370#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
372pub struct AgentEfficiencyArtifactIdentity {
373 pub schema_version: u32,
375 pub artifact_digest_kind: String,
377 pub artifact_digest: String,
379 pub candidate_version: String,
381 pub candidate_runtime_sha256: String,
383 #[serde(default)]
385 pub candidate_source_head: String,
386 #[serde(default)]
388 pub candidate_functional_head: String,
389 #[serde(default)]
391 pub candidate_checklist_head: String,
392 pub frozen_version: String,
394 pub frozen_runtime_sha256: String,
396}
397
398#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
400#[serde(rename_all = "snake_case")]
401pub enum AgentEfficiencyMetricKind {
402 TotalToolCalls,
404 ProjectAtlasCalls,
406 ProductiveFolders,
408 ProductiveFiles,
410 ProductiveRelations,
412 WrongFolders,
414 WrongFiles,
416 WrongRelations,
418 BroadReads,
420 FullReads,
422 Backtracks,
424 GrossNavigationBytes,
426 NetNavigationBytes,
428 GrossNavigationTokens,
430 NetNavigationTokens,
432 SetupWallSeconds,
434 RuntimeWallSeconds,
436 PersistentBytes,
438}
439
440#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
442pub struct AgentEfficiencyMetricComparison {
443 pub metric: AgentEfficiencyMetricKind,
445 pub candidate_median: f64,
447 pub baseline_median: f64,
449 pub candidate_maximum: f64,
451 pub baseline_maximum: f64,
453 pub median_percent_saving: Option<f64>,
455}
456
457#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
459pub struct AgentEfficiencyBreakEven {
460 pub workload: String,
462 pub wall_time_tasks: Option<u64>,
464}
465
466#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
468#[serde(rename_all = "snake_case")]
469pub enum AgentEfficiencyProviderMetricKind {
470 InputTokens,
472 CachedInputTokens,
474 CacheWriteInputTokens,
476 OutputTokens,
478 ReasoningOutputTokens,
480}
481
482#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
484pub struct AgentEfficiencyProviderMetric {
485 pub metric: AgentEfficiencyProviderMetricKind,
487 pub candidate_median: f64,
489 pub baseline_median: f64,
491 pub candidate_maximum: f64,
493 pub baseline_maximum: f64,
495 pub causal_attribution: bool,
497}
498
499#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
501pub struct AgentEfficiencyBaselineRow {
502 pub baseline: AgentEfficiencyBaseline,
504 pub state: AgentEfficiencyEvidenceState,
506 pub matched_trials: usize,
508 pub candidate_failed_trials: usize,
510 pub baseline_failed_trials: usize,
512 pub unmatched_trials: usize,
514 pub metrics: Vec<AgentEfficiencyMetricComparison>,
516 pub break_even: Vec<AgentEfficiencyBreakEven>,
518 pub provider_usage_descriptive_only: Vec<AgentEfficiencyProviderMetric>,
520}
521
522#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
524#[serde(rename_all = "snake_case")]
525pub enum AgentEfficiencyCapability {
526 Discovery,
528 SummaryAndSlice,
530 Search,
532 SymbolsAndRelations,
534 Other,
536}
537
538#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
540pub struct AgentEfficiencyCapabilityContribution {
541 pub capability: AgentEfficiencyCapability,
543 pub calls: usize,
545 pub emitted_bytes: u64,
547}
548
549#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
551pub struct AgentEfficiencyComparison {
552 pub state: AgentEfficiencyEvidenceState,
554 pub reason: Option<String>,
556 pub artifact: Option<AgentEfficiencyArtifactIdentity>,
558 pub baselines: Vec<AgentEfficiencyBaselineRow>,
560 pub capabilities: Vec<AgentEfficiencyCapabilityContribution>,
562 pub provider_counters_descriptive_only: bool,
564}
565
566impl Default for AgentEfficiencyComparison {
567 fn default() -> Self {
568 Self {
569 state: AgentEfficiencyEvidenceState::Unavailable,
570 reason: Some("benchmark artifact not supplied".to_string()),
571 artifact: None,
572 baselines: Vec::new(),
573 capabilities: Vec::new(),
574 provider_counters_descriptive_only: true,
575 }
576 }
577}
578
579#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
581pub struct TokenOverview {
582 pub estimate_kind: String,
584 pub estimator: String,
586 pub estimate_scope: String,
588 pub calls: usize,
590 pub estimated_without_projectatlas: usize,
592 pub estimated_with_projectatlas: usize,
594 pub estimated_saved: isize,
596 pub savings_rate: Option<f64>,
598 pub buckets: Vec<TokenBucketOverview>,
600 pub measured_tokens_saved: isize,
602 pub gross_modeled_tokens_avoided: isize,
604 pub deduped_modeled_tokens_avoided: isize,
606 pub tokens_avoided: isize,
608 pub legacy_gross_estimated_saved: isize,
610 pub repeated_baselines_deduped: usize,
612 #[serde(default)]
614 pub observed_file_read_replacements: usize,
615 #[serde(default)]
617 pub modeled_file_reads_avoided: usize,
618 #[serde(default)]
620 pub likely_file_reads_avoided: usize,
621 #[serde(default = "default_read_avoidance_scope")]
623 pub read_avoidance_scope: String,
624 #[serde(default = "default_read_avoidance_confidence")]
626 pub read_avoidance_confidence: String,
627 pub calibration: Option<TokenCalibrationOverview>,
629 #[serde(default)]
631 pub detail_availability: UsageDetailAvailability,
632 #[serde(default)]
634 pub agent_efficiency: AgentEfficiencyComparison,
635}
636
637#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
639#[serde(rename_all = "lowercase")]
640pub enum TokenTrendWindow {
641 Day,
643 Week,
645 Month,
647 Year,
649}
650
651impl TokenTrendWindow {
652 #[must_use]
654 pub fn parse(value: &str) -> Option<Self> {
655 match value {
656 "day" => Some(Self::Day),
657 "week" => Some(Self::Week),
658 "month" => Some(Self::Month),
659 "year" => Some(Self::Year),
660 _ => None,
661 }
662 }
663
664 #[must_use]
666 pub const fn as_str(self) -> &'static str {
667 match self {
668 Self::Day => "day",
669 Self::Week => "week",
670 Self::Month => "month",
671 Self::Year => "year",
672 }
673 }
674}
675
676impl std::fmt::Display for TokenTrendWindow {
677 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
678 formatter.write_str(self.as_str())
679 }
680}
681
682#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
684pub struct TokenTrendPeriod {
685 pub period: String,
687 pub calls: usize,
689 pub estimated_without_projectatlas: usize,
691 pub estimated_with_projectatlas: usize,
693 pub estimated_saved: isize,
695 pub savings_rate: Option<f64>,
697 pub buckets: Vec<TokenBucketOverview>,
699}
700
701#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
703pub struct TokenTrendReport {
704 pub estimate_kind: String,
706 pub estimator: String,
708 pub estimate_scope: String,
710 pub session: Option<String>,
712 pub window: TokenTrendWindow,
714 pub periods: Vec<TokenTrendPeriod>,
716 #[serde(default)]
718 pub detail_availability: UsageDetailAvailability,
719}
720
721impl UsageEvent {
722 #[must_use]
724 pub fn is_observed(&self) -> bool {
725 is_observed_event(self)
726 }
727
728 #[must_use]
730 pub fn is_modeled(&self) -> bool {
731 is_modeled_event(self)
732 }
733
734 #[must_use]
736 pub fn is_observed_file_read_replacement(&self, baseline_tokens: usize) -> bool {
737 is_observed_read_replacement_event(self, baseline_tokens)
738 }
739
740 #[must_use]
742 pub fn is_modeled_file_read_avoidance(&self, baseline_tokens: usize) -> bool {
743 is_modeled_read_avoidance_event(self, baseline_tokens)
744 }
745
746 #[must_use]
748 pub fn report_accounting_layer(&self) -> &str {
749 if self.is_observed() {
750 TOKEN_ACCOUNTING_OBSERVED_DELTA
751 } else {
752 &self.accounting_layer
753 }
754 }
755
756 #[must_use]
758 pub fn report_denominator_kind(&self) -> &str {
759 if self.is_observed() {
760 TOKEN_BASELINE_FULL_FILE
761 } else {
762 &self.denominator_kind
763 }
764 }
765
766 #[must_use]
768 pub fn report_dedupe_scope(&self) -> &str {
769 if self.is_observed() {
770 TOKEN_DEDUPE_SCOPE_EVENT
771 } else {
772 &self.dedupe_scope
773 }
774 }
775
776 #[must_use]
778 pub fn effective_baseline_identity(&self) -> Cow<'_, str> {
779 if self.baseline_identity.is_empty() {
780 Cow::Owned(default_baseline_identity(
781 &self.command,
782 self.path.as_deref(),
783 self.query.as_deref(),
784 &self.baseline_kind,
785 ))
786 } else {
787 Cow::Borrowed(&self.baseline_identity)
788 }
789 }
790
791 #[must_use]
793 pub fn effective_baseline_fingerprint(&self) -> Cow<'_, str> {
794 if self.baseline_fingerprint.is_empty() {
795 self.effective_baseline_identity()
796 } else {
797 Cow::Borrowed(&self.baseline_fingerprint)
798 }
799 }
800
801 #[must_use]
803 pub fn modeled_baseline_key(&self) -> [u8; 32] {
804 let identity = self.effective_baseline_identity();
805 let fingerprint = if self.baseline_fingerprint.is_empty() {
806 identity.as_ref()
807 } else {
808 self.baseline_fingerprint.as_str()
809 };
810 let mut hasher = blake3::Hasher::new();
811 for value in [
812 identity.as_ref(),
813 fingerprint,
814 self.denominator_kind.as_str(),
815 ] {
816 let bytes = value.as_bytes();
817 hasher.update(&(bytes.len() as u64).to_le_bytes());
818 hasher.update(bytes);
819 }
820 *hasher.finalize().as_bytes()
821 }
822}
823
824impl TokenOverview {
825 #[must_use]
827 pub fn from_events(events: &[UsageEvent]) -> Self {
828 let mut totals = BTreeMap::<TokenBucketKey, (u128, u128, u128)>::new();
829 for event in events {
830 let (Some(event_without), Some(event_with)) = (
831 event.estimated_tokens_without_projectatlas,
832 event.estimated_tokens_with_projectatlas,
833 ) else {
834 continue;
835 };
836 let entry = totals.entry(TokenBucketKey::from(event)).or_default();
837 entry.0 = entry.0.saturating_add(1);
838 entry.1 = entry.1.saturating_add(event_without as u128);
839 entry.2 = entry.2.saturating_add(event_with as u128);
840 }
841 let buckets = totals
842 .into_iter()
843 .map(|(key, (calls, without, with))| key.into_overview(calls, without, with))
844 .collect();
845 let mut overview = Self::from_buckets(buckets);
846 overview.apply_accounting_from_events(events);
847 overview
848 }
849
850 #[must_use]
852 pub fn from_estimated_totals(calls: u128, without: u128, with: u128) -> Self {
853 Self::from_buckets(vec![TokenBucketOverview::from_totals(
854 default_token_savings_bucket(),
855 default_token_provider(),
856 default_token_model(),
857 default_tokenizer_backend(),
858 default_token_accuracy(),
859 default_token_baseline_kind(),
860 default_token_confidence(),
861 default_accounting_layer(),
862 default_estimate_method(),
863 default_denominator_kind(),
864 default_dedupe_scope(),
865 calls,
866 without,
867 with,
868 )])
869 }
870
871 #[must_use]
873 pub fn from_buckets(buckets: Vec<TokenBucketOverview>) -> Self {
874 let calls = buckets.iter().fold(0u128, |acc, bucket| {
875 acc.saturating_add(bucket.calls as u128)
876 });
877 let without = buckets.iter().fold(0u128, |acc, bucket| {
878 acc.saturating_add(bucket.estimated_without_projectatlas as u128)
879 });
880 let with = buckets.iter().fold(0u128, |acc, bucket| {
881 acc.saturating_add(bucket.estimated_with_projectatlas as u128)
882 });
883 let saved = aggregate_token_delta(without, with);
884 let savings_rate = if without == 0 {
885 None
886 } else {
887 Some((without as f64 - with as f64) / without as f64)
888 };
889 let measured_tokens_saved = measured_tokens_saved_from_buckets(&buckets);
890 let gross_modeled_tokens_avoided = modeled_tokens_saved_from_buckets(&buckets);
891 let tokens_avoided =
892 saturating_isize_add(measured_tokens_saved, gross_modeled_tokens_avoided);
893 Self {
894 estimate_kind: TOKEN_ESTIMATE_KIND.to_string(),
895 estimator: TOKEN_ESTIMATOR.to_string(),
896 estimate_scope: TOKEN_ESTIMATE_SCOPE.to_string(),
897 calls: saturating_u128_to_usize(calls),
898 estimated_without_projectatlas: saturating_u128_to_usize(without),
899 estimated_with_projectatlas: saturating_u128_to_usize(with),
900 estimated_saved: saved,
901 savings_rate,
902 measured_tokens_saved,
903 gross_modeled_tokens_avoided,
904 deduped_modeled_tokens_avoided: gross_modeled_tokens_avoided,
905 tokens_avoided,
906 legacy_gross_estimated_saved: saved,
907 repeated_baselines_deduped: 0,
908 observed_file_read_replacements: 0,
909 modeled_file_reads_avoided: 0,
910 likely_file_reads_avoided: 0,
911 read_avoidance_scope: READ_AVOIDANCE_SCOPE.to_string(),
912 read_avoidance_confidence: READ_AVOIDANCE_CONFIDENCE_NOT_RECORDED.to_string(),
913 calibration: None,
914 detail_availability: UsageDetailAvailability::Retained,
915 agent_efficiency: AgentEfficiencyComparison::default(),
916 buckets,
917 }
918 }
919
920 pub fn set_calibration(&mut self, calibration: TokenCalibrationOverview) {
922 self.calibration = Some(calibration);
923 }
924
925 pub fn set_agent_efficiency(&mut self, comparison: AgentEfficiencyComparison) {
927 self.agent_efficiency = comparison;
928 }
929
930 pub fn apply_accounting_totals(&mut self, totals: TokenAccountingTotals) {
932 self.measured_tokens_saved = saturating_i128_to_isize(totals.measured_tokens_saved);
933 self.gross_modeled_tokens_avoided =
934 saturating_i128_to_isize(totals.gross_modeled_tokens_avoided);
935 self.deduped_modeled_tokens_avoided =
936 saturating_i128_to_isize(totals.deduped_modeled_tokens_avoided);
937 self.tokens_avoided = saturating_isize_add(
938 self.measured_tokens_saved,
939 self.deduped_modeled_tokens_avoided,
940 );
941 self.repeated_baselines_deduped =
942 saturating_u128_to_usize(totals.repeated_baselines_deduped);
943 self.observed_file_read_replacements =
944 saturating_u128_to_usize(totals.observed_file_read_replacements);
945 self.modeled_file_reads_avoided =
946 saturating_u128_to_usize(totals.modeled_file_reads_avoided);
947 self.likely_file_reads_avoided = self
948 .observed_file_read_replacements
949 .saturating_add(self.modeled_file_reads_avoided);
950 self.read_avoidance_confidence = read_avoidance_confidence_for(
951 self.observed_file_read_replacements,
952 self.modeled_file_reads_avoided,
953 )
954 .to_string();
955 }
956
957 pub const fn set_detail_availability(&mut self, availability: UsageDetailAvailability) {
959 self.detail_availability = availability;
960 }
961
962 pub fn apply_accounting_from_events(&mut self, events: &[UsageEvent]) {
964 let summary = TokenAccountingSummary::from_events(events);
965 self.measured_tokens_saved = summary.measured_tokens_saved;
966 self.gross_modeled_tokens_avoided = summary.gross_modeled_tokens_avoided;
967 self.deduped_modeled_tokens_avoided = summary.deduped_modeled_tokens_avoided;
968 self.tokens_avoided = summary.tokens_avoided;
969 self.repeated_baselines_deduped = summary.repeated_baselines_deduped;
970 self.observed_file_read_replacements = summary.observed_file_read_replacements;
971 self.modeled_file_reads_avoided = summary.modeled_file_reads_avoided;
972 self.likely_file_reads_avoided = summary.likely_file_reads_avoided;
973 self.read_avoidance_confidence = read_avoidance_confidence_for(
974 self.observed_file_read_replacements,
975 self.modeled_file_reads_avoided,
976 )
977 .to_string();
978 }
979}
980
981impl TokenBucketOverview {
982 #[must_use]
984 #[allow(clippy::too_many_arguments)]
985 pub fn from_totals(
986 token_savings_bucket: String,
987 provider: String,
988 model: String,
989 tokenizer_backend: String,
990 accuracy: String,
991 baseline_kind: String,
992 confidence: String,
993 accounting_layer: String,
994 estimate_method: String,
995 denominator_kind: String,
996 dedupe_scope: String,
997 calls: u128,
998 without: u128,
999 with: u128,
1000 ) -> Self {
1001 let estimated_saved = aggregate_token_delta(without, with);
1002 let savings_rate = if without == 0 {
1003 None
1004 } else {
1005 Some((without as f64 - with as f64) / without as f64)
1006 };
1007 Self {
1008 token_savings_bucket,
1009 provider,
1010 model,
1011 tokenizer_backend,
1012 accuracy,
1013 baseline_kind,
1014 confidence,
1015 calls: saturating_u128_to_usize(calls),
1016 estimated_without_projectatlas: saturating_u128_to_usize(without),
1017 estimated_with_projectatlas: saturating_u128_to_usize(with),
1018 estimated_saved,
1019 savings_rate,
1020 accounting_layer,
1021 estimate_method,
1022 denominator_kind,
1023 dedupe_scope,
1024 }
1025 }
1026}
1027
1028impl TokenTrendPeriod {
1029 #[must_use]
1031 pub fn from_totals(period: String, calls: u128, without: u128, with: u128) -> Self {
1032 let bucket = TokenBucketOverview::from_totals(
1033 default_token_savings_bucket(),
1034 default_token_provider(),
1035 default_token_model(),
1036 default_tokenizer_backend(),
1037 default_token_accuracy(),
1038 default_token_baseline_kind(),
1039 default_token_confidence(),
1040 default_accounting_layer(),
1041 default_estimate_method(),
1042 default_denominator_kind(),
1043 default_dedupe_scope(),
1044 calls,
1045 without,
1046 with,
1047 );
1048 Self::from_buckets(period, vec![bucket])
1049 }
1050
1051 #[must_use]
1053 pub fn from_buckets(period: String, buckets: Vec<TokenBucketOverview>) -> Self {
1054 let calls = buckets.iter().fold(0u128, |acc, bucket| {
1055 acc.saturating_add(bucket.calls as u128)
1056 });
1057 let without = buckets.iter().fold(0u128, |acc, bucket| {
1058 acc.saturating_add(bucket.estimated_without_projectatlas as u128)
1059 });
1060 let with = buckets.iter().fold(0u128, |acc, bucket| {
1061 acc.saturating_add(bucket.estimated_with_projectatlas as u128)
1062 });
1063 let saved = aggregate_token_delta(without, with);
1064 let savings_rate = if without == 0 {
1065 None
1066 } else {
1067 Some((without as f64 - with as f64) / without as f64)
1068 };
1069 Self {
1070 period,
1071 calls: saturating_u128_to_usize(calls),
1072 estimated_without_projectatlas: saturating_u128_to_usize(without),
1073 estimated_with_projectatlas: saturating_u128_to_usize(with),
1074 estimated_saved: saved,
1075 savings_rate,
1076 buckets,
1077 }
1078 }
1079}
1080
1081impl TokenTrendReport {
1082 #[must_use]
1084 pub fn new(
1085 session: Option<String>,
1086 window: TokenTrendWindow,
1087 periods: Vec<TokenTrendPeriod>,
1088 ) -> Self {
1089 Self {
1090 estimate_kind: TOKEN_ESTIMATE_KIND.to_string(),
1091 estimator: TOKEN_ESTIMATOR.to_string(),
1092 estimate_scope: TOKEN_ESTIMATE_SCOPE.to_string(),
1093 session,
1094 window,
1095 periods,
1096 detail_availability: UsageDetailAvailability::Retained,
1097 }
1098 }
1099
1100 pub const fn set_detail_availability(&mut self, availability: UsageDetailAvailability) {
1102 self.detail_availability = availability;
1103 }
1104}
1105
1106#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
1108struct TokenBucketKey {
1109 token_savings_bucket: String,
1111 provider: String,
1113 model: String,
1115 tokenizer_backend: String,
1117 accuracy: String,
1119 baseline_kind: String,
1121 confidence: String,
1123 accounting_layer: String,
1125 estimate_method: String,
1127 denominator_kind: String,
1129 dedupe_scope: String,
1131}
1132
1133#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
1135struct ModeledBaselineKey {
1136 session_id: String,
1138 baseline_identity: String,
1140 baseline_fingerprint: String,
1142 denominator_kind: String,
1144}
1145
1146#[derive(Default)]
1148struct ModeledBaselineTotals {
1149 calls: usize,
1151 baseline_without_projectatlas: usize,
1153 emitted_with_projectatlas: u128,
1155}
1156
1157#[derive(Default)]
1159struct TokenAccountingSummary {
1160 measured_tokens_saved: isize,
1162 gross_modeled_tokens_avoided: isize,
1164 deduped_modeled_tokens_avoided: isize,
1166 tokens_avoided: isize,
1168 repeated_baselines_deduped: usize,
1170 observed_file_read_replacements: usize,
1172 modeled_file_reads_avoided: usize,
1174 likely_file_reads_avoided: usize,
1176}
1177
1178impl TokenAccountingSummary {
1179 fn from_events(events: &[UsageEvent]) -> Self {
1181 let mut measured_tokens_saved = 0isize;
1182 let mut gross_modeled_tokens_avoided = 0isize;
1183 let mut event_scoped_modeled_tokens_avoided = 0isize;
1184 let mut observed_file_read_replacements = 0usize;
1185 let mut modeled_file_reads_avoided = 0usize;
1186 let mut modeled_baselines = BTreeMap::<ModeledBaselineKey, ModeledBaselineTotals>::new();
1187
1188 for event in events {
1189 let (Some(without), Some(with)) = (
1190 event.estimated_tokens_without_projectatlas,
1191 event.estimated_tokens_with_projectatlas,
1192 ) else {
1193 continue;
1194 };
1195 let delta = token_delta(without, with);
1196 if is_observed_event(event) {
1197 measured_tokens_saved = saturating_isize_add(measured_tokens_saved, delta);
1198 if is_observed_read_replacement_event(event, without) {
1199 observed_file_read_replacements =
1200 observed_file_read_replacements.saturating_add(1);
1201 }
1202 continue;
1203 }
1204 if !is_modeled_event(event) {
1205 continue;
1206 }
1207 if is_modeled_read_avoidance_event(event, without) {
1208 modeled_file_reads_avoided = modeled_file_reads_avoided.saturating_add(1);
1209 }
1210 gross_modeled_tokens_avoided =
1211 saturating_isize_add(gross_modeled_tokens_avoided, delta);
1212 if event.dedupe_scope == TOKEN_DEDUPE_SCOPE_EVENT {
1213 event_scoped_modeled_tokens_avoided =
1214 saturating_isize_add(event_scoped_modeled_tokens_avoided, delta);
1215 continue;
1216 }
1217 let entry = modeled_baselines
1218 .entry(ModeledBaselineKey::from_event(event))
1219 .or_default();
1220 entry.calls = entry.calls.saturating_add(1);
1221 entry.baseline_without_projectatlas = entry.baseline_without_projectatlas.max(without);
1222 entry.emitted_with_projectatlas =
1223 entry.emitted_with_projectatlas.saturating_add(with as u128);
1224 }
1225
1226 let mut deduped_modeled_tokens_avoided = event_scoped_modeled_tokens_avoided;
1227 let mut repeated_baselines_deduped = 0usize;
1228 for totals in modeled_baselines.values() {
1229 if totals.calls > 1 {
1230 repeated_baselines_deduped =
1231 repeated_baselines_deduped.saturating_add(totals.calls.saturating_sub(1));
1232 }
1233 let delta = aggregate_token_delta(
1234 totals.baseline_without_projectatlas as u128,
1235 totals.emitted_with_projectatlas,
1236 );
1237 deduped_modeled_tokens_avoided =
1238 saturating_isize_add(deduped_modeled_tokens_avoided, delta);
1239 }
1240 let tokens_avoided =
1241 saturating_isize_add(measured_tokens_saved, deduped_modeled_tokens_avoided);
1242 let likely_file_reads_avoided =
1243 observed_file_read_replacements.saturating_add(modeled_file_reads_avoided);
1244 Self {
1245 measured_tokens_saved,
1246 gross_modeled_tokens_avoided,
1247 deduped_modeled_tokens_avoided,
1248 tokens_avoided,
1249 repeated_baselines_deduped,
1250 observed_file_read_replacements,
1251 modeled_file_reads_avoided,
1252 likely_file_reads_avoided,
1253 }
1254 }
1255}
1256
1257impl ModeledBaselineKey {
1258 fn from_event(event: &UsageEvent) -> Self {
1260 let identity = if event.baseline_identity.is_empty() {
1261 default_baseline_identity(
1262 &event.command,
1263 event.path.as_deref(),
1264 event.query.as_deref(),
1265 &event.baseline_kind,
1266 )
1267 } else {
1268 event.baseline_identity.clone()
1269 };
1270 let fingerprint = if event.baseline_fingerprint.is_empty() {
1271 identity.clone()
1272 } else {
1273 event.baseline_fingerprint.clone()
1274 };
1275 Self {
1276 session_id: event.session_id.clone(),
1277 baseline_identity: identity,
1278 baseline_fingerprint: fingerprint,
1279 denominator_kind: event.denominator_kind.clone(),
1280 }
1281 }
1282}
1283
1284impl TokenBucketKey {
1285 fn from(event: &UsageEvent) -> Self {
1287 let observed = is_observed_event(event);
1288 Self {
1289 token_savings_bucket: event.token_savings_bucket.clone(),
1290 provider: event.provider.clone(),
1291 model: event.model.clone(),
1292 tokenizer_backend: event.tokenizer_backend.clone(),
1293 accuracy: event.accuracy.clone(),
1294 baseline_kind: event.baseline_kind.clone(),
1295 confidence: event.confidence.clone(),
1296 accounting_layer: if observed {
1297 TOKEN_ACCOUNTING_OBSERVED_DELTA.to_string()
1298 } else {
1299 event.accounting_layer.clone()
1300 },
1301 estimate_method: event.estimate_method.clone(),
1302 denominator_kind: if observed {
1303 TOKEN_BASELINE_FULL_FILE.to_string()
1304 } else {
1305 event.denominator_kind.clone()
1306 },
1307 dedupe_scope: if observed {
1308 TOKEN_DEDUPE_SCOPE_EVENT.to_string()
1309 } else {
1310 event.dedupe_scope.clone()
1311 },
1312 }
1313 }
1314
1315 fn into_overview(self, calls: u128, without: u128, with: u128) -> TokenBucketOverview {
1317 TokenBucketOverview::from_totals(
1318 self.token_savings_bucket,
1319 self.provider,
1320 self.model,
1321 self.tokenizer_backend,
1322 self.accuracy,
1323 self.baseline_kind,
1324 self.confidence,
1325 self.accounting_layer,
1326 self.estimate_method,
1327 self.denominator_kind,
1328 self.dedupe_scope,
1329 calls,
1330 without,
1331 with,
1332 )
1333 }
1334}
1335
1336#[must_use]
1338pub fn usage_from_text(
1339 session_id: &str,
1340 command: &str,
1341 path: Option<String>,
1342 query: Option<String>,
1343 baseline_text: &str,
1344 projectatlas_text: &str,
1345) -> UsageEvent {
1346 let without = estimate_tokens(baseline_text);
1347 let with = estimate_tokens(projectatlas_text);
1348 usage_from_estimates_with_accounting(
1349 session_id,
1350 command,
1351 path,
1352 query,
1353 without,
1354 with,
1355 TOKEN_BUCKET_FULL_FILE_COMPRESSION,
1356 TOKEN_BASELINE_FULL_FILE,
1357 TOKEN_CONFIDENCE_OBSERVED,
1358 TOKEN_ACCOUNTING_OBSERVED_DELTA,
1359 TOKEN_BASELINE_FULL_FILE,
1360 TOKEN_DEDUPE_SCOPE_EVENT,
1361 )
1362}
1363
1364#[must_use]
1366pub fn usage_from_estimates(
1367 session_id: &str,
1368 command: &str,
1369 path: Option<String>,
1370 query: Option<String>,
1371 estimated_without_projectatlas: usize,
1372 estimated_with_projectatlas: usize,
1373) -> UsageEvent {
1374 usage_from_estimates_with_accounting(
1375 session_id,
1376 command,
1377 path,
1378 query,
1379 estimated_without_projectatlas,
1380 estimated_with_projectatlas,
1381 TOKEN_BUCKET_NAVIGATION_AVOIDANCE,
1382 TOKEN_BASELINE_SELECTED_CANDIDATES,
1383 TOKEN_CONFIDENCE_INFERRED,
1384 TOKEN_ACCOUNTING_MODELED_AVOIDANCE,
1385 TOKEN_BASELINE_SELECTED_CANDIDATES,
1386 TOKEN_DEDUPE_SCOPE_SESSION,
1387 )
1388}
1389
1390#[must_use]
1392#[allow(clippy::too_many_arguments)]
1393pub fn usage_from_estimates_with_context(
1394 session_id: &str,
1395 command: &str,
1396 path: Option<String>,
1397 query: Option<String>,
1398 estimated_without_projectatlas: usize,
1399 estimated_with_projectatlas: usize,
1400 token_savings_bucket: &str,
1401 baseline_kind: &str,
1402 confidence: &str,
1403) -> UsageEvent {
1404 usage_from_estimates_with_accounting(
1405 session_id,
1406 command,
1407 path,
1408 query,
1409 estimated_without_projectatlas,
1410 estimated_with_projectatlas,
1411 token_savings_bucket,
1412 baseline_kind,
1413 confidence,
1414 if token_savings_bucket == TOKEN_BUCKET_FULL_FILE_COMPRESSION {
1415 TOKEN_ACCOUNTING_OBSERVED_DELTA
1416 } else {
1417 TOKEN_ACCOUNTING_MODELED_AVOIDANCE
1418 },
1419 baseline_kind,
1420 if token_savings_bucket == TOKEN_BUCKET_FULL_FILE_COMPRESSION {
1421 TOKEN_DEDUPE_SCOPE_EVENT
1422 } else {
1423 TOKEN_DEDUPE_SCOPE_SESSION
1424 },
1425 )
1426}
1427
1428#[must_use]
1430#[allow(clippy::too_many_arguments)]
1431pub fn usage_from_estimates_with_accounting(
1432 session_id: &str,
1433 command: &str,
1434 path: Option<String>,
1435 query: Option<String>,
1436 estimated_without_projectatlas: usize,
1437 estimated_with_projectatlas: usize,
1438 token_savings_bucket: &str,
1439 baseline_kind: &str,
1440 confidence: &str,
1441 accounting_layer: &str,
1442 denominator_kind: &str,
1443 dedupe_scope: &str,
1444) -> UsageEvent {
1445 let baseline_identity =
1446 default_baseline_identity(command, path.as_deref(), query.as_deref(), baseline_kind);
1447 let baseline_fingerprint = baseline_identity.clone();
1448 UsageEvent {
1449 session_id: session_id.to_string(),
1450 command: command.to_string(),
1451 path,
1452 query,
1453 estimated_tokens_without_projectatlas: Some(estimated_without_projectatlas),
1454 estimated_tokens_with_projectatlas: Some(estimated_with_projectatlas),
1455 estimated_tokens_saved: Some(token_delta(
1456 estimated_without_projectatlas,
1457 estimated_with_projectatlas,
1458 )),
1459 token_savings_bucket: token_savings_bucket.to_string(),
1460 provider: default_token_provider(),
1461 model: default_token_model(),
1462 tokenizer_backend: default_tokenizer_backend(),
1463 accuracy: default_token_accuracy(),
1464 baseline_kind: baseline_kind.to_string(),
1465 confidence: confidence.to_string(),
1466 calculation_trace: default_token_trace(),
1467 accounting_layer: accounting_layer.to_string(),
1468 estimate_method: default_estimate_method(),
1469 denominator_kind: denominator_kind.to_string(),
1470 baseline_identity,
1471 baseline_fingerprint,
1472 dedupe_scope: dedupe_scope.to_string(),
1473 }
1474}
1475
1476#[must_use]
1478pub fn default_token_savings_bucket() -> String {
1479 TOKEN_BUCKET_NAVIGATION_AVOIDANCE.to_string()
1480}
1481
1482#[must_use]
1484pub fn default_token_provider() -> String {
1485 TOKEN_PROVIDER_HEURISTIC.to_string()
1486}
1487
1488#[must_use]
1490pub fn default_token_model() -> String {
1491 TOKEN_MODEL_UNKNOWN.to_string()
1492}
1493
1494#[must_use]
1496pub fn default_tokenizer_backend() -> String {
1497 TOKENIZER_BACKEND_HEURISTIC.to_string()
1498}
1499
1500#[must_use]
1502pub fn default_token_accuracy() -> String {
1503 TOKEN_ACCURACY_HEURISTIC.to_string()
1504}
1505
1506#[must_use]
1508pub fn default_token_baseline_kind() -> String {
1509 TOKEN_BASELINE_SELECTED_CANDIDATES.to_string()
1510}
1511
1512#[must_use]
1514pub fn default_token_confidence() -> String {
1515 TOKEN_CONFIDENCE_INFERRED.to_string()
1516}
1517
1518#[must_use]
1520pub fn default_token_trace() -> String {
1521 TOKEN_TRACE_HEURISTIC.to_string()
1522}
1523
1524#[must_use]
1526pub fn default_accounting_layer() -> String {
1527 TOKEN_ACCOUNTING_MODELED_AVOIDANCE.to_string()
1528}
1529
1530#[must_use]
1532pub fn default_estimate_method() -> String {
1533 TOKEN_ESTIMATE_METHOD_HEURISTIC.to_string()
1534}
1535
1536#[must_use]
1538pub fn default_denominator_kind() -> String {
1539 TOKEN_BASELINE_SELECTED_CANDIDATES.to_string()
1540}
1541
1542#[must_use]
1544pub fn default_dedupe_scope() -> String {
1545 TOKEN_DEDUPE_SCOPE_SESSION.to_string()
1546}
1547
1548#[must_use]
1550pub fn default_read_avoidance_scope() -> String {
1551 READ_AVOIDANCE_SCOPE.to_string()
1552}
1553
1554#[must_use]
1556pub fn default_read_avoidance_confidence() -> String {
1557 READ_AVOIDANCE_CONFIDENCE_NOT_RECORDED.to_string()
1558}
1559
1560#[must_use]
1562pub fn default_baseline_identity(
1563 command: &str,
1564 path: Option<&str>,
1565 query: Option<&str>,
1566 baseline_kind: &str,
1567) -> String {
1568 format!(
1569 "{baseline_kind}:command={command}:path={path}:query={query}",
1570 path = path.unwrap_or("*"),
1571 query = query.unwrap_or("*")
1572 )
1573}
1574
1575fn token_delta(without: usize, with: usize) -> isize {
1577 let without = isize::try_from(without).unwrap_or(isize::MAX);
1578 let with = isize::try_from(with).unwrap_or(isize::MAX);
1579 without.saturating_sub(with)
1580}
1581
1582fn aggregate_token_delta(without: u128, with: u128) -> isize {
1584 if without >= with {
1585 let delta = without - with;
1586 if delta > isize::MAX as u128 {
1587 isize::MAX
1588 } else {
1589 delta as isize
1590 }
1591 } else {
1592 let delta = with - without;
1593 if delta > isize::MAX as u128 {
1594 isize::MIN
1595 } else {
1596 -(delta as isize)
1597 }
1598 }
1599}
1600
1601fn saturating_u128_to_usize(value: u128) -> usize {
1603 if value > usize::MAX as u128 {
1604 usize::MAX
1605 } else {
1606 value as usize
1607 }
1608}
1609
1610fn saturating_i128_to_isize(value: i128) -> isize {
1612 if value > isize::MAX as i128 {
1613 isize::MAX
1614 } else if value < isize::MIN as i128 {
1615 isize::MIN
1616 } else {
1617 value as isize
1618 }
1619}
1620
1621fn saturating_isize_add(left: isize, right: isize) -> isize {
1623 left.saturating_add(right)
1624}
1625
1626fn measured_tokens_saved_from_buckets(buckets: &[TokenBucketOverview]) -> isize {
1628 buckets
1629 .iter()
1630 .filter(|bucket| is_observed_bucket(bucket))
1631 .fold(0isize, |acc, bucket| {
1632 saturating_isize_add(acc, bucket.estimated_saved)
1633 })
1634}
1635
1636fn modeled_tokens_saved_from_buckets(buckets: &[TokenBucketOverview]) -> isize {
1638 buckets
1639 .iter()
1640 .filter(|bucket| is_modeled_bucket(bucket))
1641 .fold(0isize, |acc, bucket| {
1642 saturating_isize_add(acc, bucket.estimated_saved)
1643 })
1644}
1645
1646fn is_observed_event(event: &UsageEvent) -> bool {
1648 event.accounting_layer == TOKEN_ACCOUNTING_OBSERVED_DELTA
1649 || event.token_savings_bucket == TOKEN_BUCKET_FULL_FILE_COMPRESSION
1650 || event.confidence == TOKEN_CONFIDENCE_OBSERVED
1651}
1652
1653fn is_modeled_event(event: &UsageEvent) -> bool {
1655 event.accounting_layer == TOKEN_ACCOUNTING_MODELED_AVOIDANCE || !is_observed_event(event)
1656}
1657
1658fn is_observed_read_replacement_event(event: &UsageEvent, baseline_tokens: usize) -> bool {
1660 baseline_tokens > 0
1661 && matches!(
1662 event.command.as_str(),
1663 TOKEN_COMMAND_SUMMARY
1664 | TOKEN_COMMAND_OUTLINE
1665 | TOKEN_COMMAND_SLICE
1666 | TOKEN_COMMAND_SYMBOL_SLICE
1667 | TOKEN_COMMAND_MCP_FILE_SUMMARY
1668 | TOKEN_COMMAND_MCP_OUTLINE
1669 | TOKEN_COMMAND_MCP_SLICE
1670 )
1671}
1672
1673fn is_modeled_read_avoidance_event(event: &UsageEvent, baseline_tokens: usize) -> bool {
1675 baseline_tokens > 0
1676 && matches!(
1677 event.command.as_str(),
1678 TOKEN_COMMAND_SEARCH | TOKEN_COMMAND_MCP_SEARCH
1679 )
1680 && event.denominator_kind == TOKEN_BASELINE_SELECTED_CANDIDATES
1681}
1682
1683fn read_avoidance_confidence_for(
1685 observed_file_read_replacements: usize,
1686 modeled_file_reads_avoided: usize,
1687) -> &'static str {
1688 if observed_file_read_replacements == 0 && modeled_file_reads_avoided == 0 {
1689 READ_AVOIDANCE_CONFIDENCE_NOT_RECORDED
1690 } else if modeled_file_reads_avoided == 0 {
1691 READ_AVOIDANCE_CONFIDENCE_OBSERVED
1692 } else {
1693 READ_AVOIDANCE_CONFIDENCE_MODELED
1694 }
1695}
1696
1697fn is_observed_bucket(bucket: &TokenBucketOverview) -> bool {
1699 bucket.accounting_layer == TOKEN_ACCOUNTING_OBSERVED_DELTA
1700 || bucket.token_savings_bucket == TOKEN_BUCKET_FULL_FILE_COMPRESSION
1701 || bucket.confidence == TOKEN_CONFIDENCE_OBSERVED
1702}
1703
1704fn is_modeled_bucket(bucket: &TokenBucketOverview) -> bool {
1706 bucket.accounting_layer == TOKEN_ACCOUNTING_MODELED_AVOIDANCE || !is_observed_bucket(bucket)
1707}
1708
1709#[cfg(test)]
1710mod tests {
1711 use super::{
1712 AgentEfficiencyEvidenceState, READ_AVOIDANCE_CONFIDENCE_MODELED,
1713 READ_AVOIDANCE_CONFIDENCE_NOT_RECORDED, READ_AVOIDANCE_CONFIDENCE_OBSERVED,
1714 READ_AVOIDANCE_SCOPE, TOKEN_BUCKET_FULL_FILE_COMPRESSION,
1715 TOKEN_BUCKET_NAVIGATION_AVOIDANCE, TOKEN_ESTIMATE_KIND, TOKEN_ESTIMATE_SCOPE,
1716 TOKEN_ESTIMATOR, TelemetryContractError, TokenAccountingTotals, TokenOverview,
1717 TokenTrendReport, TokenTrendWindow, UsageDetailAvailability, UsageInstanceId,
1718 UsageInstanceOwner, usage_from_estimates, usage_from_text,
1719 };
1720 use std::io;
1721
1722 fn require_eq<T: std::fmt::Debug + PartialEq>(
1723 actual: &T,
1724 expected: &T,
1725 label: &str,
1726 ) -> Result<(), Box<dyn std::error::Error>> {
1727 if actual == expected {
1728 Ok(())
1729 } else {
1730 Err(io::Error::other(format!(
1731 "{label} mismatch: expected {expected:?}, got {actual:?}"
1732 ))
1733 .into())
1734 }
1735 }
1736
1737 #[test]
1738 fn usage_instance_ids_validate_and_round_trip() {
1739 let bytes = [7; 16];
1740 let identity = UsageInstanceId::from_bytes(bytes);
1741 assert_eq!(identity.map(UsageInstanceId::as_bytes), Ok(bytes));
1742 assert_eq!(
1743 UsageInstanceId::from_bytes([0; 16]),
1744 Err(TelemetryContractError::ZeroUsageInstanceId)
1745 );
1746 }
1747
1748 #[test]
1749 fn usage_states_parse_and_missing_report_state_fails_honest()
1750 -> Result<(), Box<dyn std::error::Error>> {
1751 for (value, expected) in [
1752 ("cli_invocation", UsageInstanceOwner::CliInvocation),
1753 ("mcp_process", UsageInstanceOwner::McpProcess),
1754 ("library_handle", UsageInstanceOwner::LibraryHandle),
1755 ("migrated_legacy", UsageInstanceOwner::MigratedLegacy),
1756 ] {
1757 require_eq(
1758 &UsageInstanceOwner::parse(value),
1759 &Some(expected),
1760 "usage instance owner parse",
1761 )?;
1762 require_eq(&expected.as_str(), &value, "usage instance owner encoding")?;
1763 }
1764 require_eq(
1765 &UsageInstanceOwner::parse("unknown"),
1766 &None,
1767 "unknown usage instance owner",
1768 )?;
1769
1770 for (value, expected) in [
1771 ("retained", UsageDetailAvailability::Retained),
1772 ("partial", UsageDetailAvailability::Partial),
1773 ("expired", UsageDetailAvailability::Expired),
1774 ("unavailable", UsageDetailAvailability::Unavailable),
1775 ] {
1776 require_eq(
1777 &UsageDetailAvailability::parse(value),
1778 &Some(expected),
1779 "detail availability parse",
1780 )?;
1781 require_eq(&expected.as_str(), &value, "detail availability encoding")?;
1782 }
1783 require_eq(
1784 &UsageDetailAvailability::parse("unknown"),
1785 &None,
1786 "unknown detail availability",
1787 )?;
1788 require_eq(
1789 &UsageDetailAvailability::default(),
1790 &UsageDetailAvailability::Unavailable,
1791 "default detail availability",
1792 )?;
1793
1794 let overview = TokenOverview::from_events(&[]);
1795 require_eq(
1796 &overview.detail_availability,
1797 &UsageDetailAvailability::Retained,
1798 "new overview detail availability",
1799 )?;
1800 let mut overview_value = serde_json::to_value(overview)?;
1801 let overview_object = overview_value
1802 .as_object_mut()
1803 .ok_or_else(|| io::Error::other("serialized token overview was not an object"))?;
1804 overview_object.remove("detail_availability");
1805 overview_object.remove("agent_efficiency");
1806 let decoded_overview: TokenOverview = serde_json::from_value(overview_value)?;
1807 require_eq(
1808 &decoded_overview.detail_availability,
1809 &UsageDetailAvailability::Unavailable,
1810 "missing overview detail availability",
1811 )?;
1812 require_eq(
1813 &decoded_overview.agent_efficiency.state,
1814 &AgentEfficiencyEvidenceState::Unavailable,
1815 "missing agent-efficiency evidence state",
1816 )?;
1817 require_eq(
1818 &decoded_overview.agent_efficiency.baselines,
1819 &Vec::new(),
1820 "missing agent-efficiency baseline rows",
1821 )?;
1822 require_eq(
1823 &AgentEfficiencyEvidenceState::Partial.as_str(),
1824 &"partial",
1825 "agent-efficiency evidence encoding",
1826 )?;
1827
1828 let trends = TokenTrendReport::new(None, TokenTrendWindow::Day, Vec::new());
1829 require_eq(
1830 &trends.detail_availability,
1831 &UsageDetailAvailability::Retained,
1832 "new trend detail availability",
1833 )?;
1834 let mut trends_value = serde_json::to_value(trends)?;
1835 let trends_object = trends_value
1836 .as_object_mut()
1837 .ok_or_else(|| io::Error::other("serialized token trends were not an object"))?;
1838 trends_object.remove("detail_availability");
1839 let decoded_trends: TokenTrendReport = serde_json::from_value(trends_value)?;
1840 require_eq(
1841 &decoded_trends.detail_availability,
1842 &UsageDetailAvailability::Unavailable,
1843 "missing trend detail availability",
1844 )?;
1845 Ok(())
1846 }
1847
1848 #[test]
1849 fn modeled_baseline_keys_preserve_legacy_fallback_and_component_boundaries() {
1850 let event = usage_from_estimates(
1851 "session",
1852 "search",
1853 Some("src/lib.rs".to_string()),
1854 Some("needle".to_string()),
1855 100,
1856 20,
1857 );
1858 let expected_key = event.modeled_baseline_key();
1859 assert_eq!(
1860 event.effective_baseline_identity().as_ref(),
1861 event.baseline_identity
1862 );
1863 assert_eq!(
1864 event.effective_baseline_fingerprint().as_ref(),
1865 event.baseline_fingerprint
1866 );
1867
1868 let mut legacy = event.clone();
1869 legacy.baseline_identity.clear();
1870 legacy.baseline_fingerprint.clear();
1871 assert_eq!(legacy.modeled_baseline_key(), expected_key);
1872
1873 let mut changed_fingerprint = event.clone();
1874 changed_fingerprint
1875 .baseline_fingerprint
1876 .push_str("-changed");
1877 assert_ne!(changed_fingerprint.modeled_baseline_key(), expected_key);
1878
1879 let mut left = event.clone();
1880 left.baseline_identity = "ab".to_string();
1881 left.baseline_fingerprint = "c".to_string();
1882 let mut right = event;
1883 right.baseline_identity = "a".to_string();
1884 right.baseline_fingerprint = "bc".to_string();
1885 assert_ne!(left.modeled_baseline_key(), right.modeled_baseline_key());
1886 }
1887
1888 #[test]
1889 fn wide_accounting_totals_narrow_only_at_the_report_boundary() {
1890 let mut overview = TokenOverview::from_events(&[]);
1891 overview.apply_accounting_totals(TokenAccountingTotals {
1892 measured_tokens_saved: 7,
1893 gross_modeled_tokens_avoided: 100,
1894 deduped_modeled_tokens_avoided: 30,
1895 repeated_baselines_deduped: 2,
1896 observed_file_read_replacements: 1,
1897 modeled_file_reads_avoided: 3,
1898 });
1899 assert_eq!(overview.measured_tokens_saved, 7);
1900 assert_eq!(overview.gross_modeled_tokens_avoided, 100);
1901 assert_eq!(overview.deduped_modeled_tokens_avoided, 30);
1902 assert_eq!(overview.tokens_avoided, 37);
1903 assert_eq!(overview.repeated_baselines_deduped, 2);
1904 assert_eq!(overview.observed_file_read_replacements, 1);
1905 assert_eq!(overview.modeled_file_reads_avoided, 3);
1906 assert_eq!(overview.likely_file_reads_avoided, 4);
1907 assert_eq!(
1908 overview.read_avoidance_confidence,
1909 READ_AVOIDANCE_CONFIDENCE_MODELED
1910 );
1911
1912 overview.apply_accounting_totals(TokenAccountingTotals {
1913 measured_tokens_saved: i128::MAX,
1914 gross_modeled_tokens_avoided: i128::MIN,
1915 deduped_modeled_tokens_avoided: i128::MAX,
1916 repeated_baselines_deduped: u128::MAX,
1917 observed_file_read_replacements: u128::MAX,
1918 modeled_file_reads_avoided: u128::MAX,
1919 });
1920 assert_eq!(overview.measured_tokens_saved, isize::MAX);
1921 assert_eq!(overview.gross_modeled_tokens_avoided, isize::MIN);
1922 assert_eq!(overview.deduped_modeled_tokens_avoided, isize::MAX);
1923 assert_eq!(overview.tokens_avoided, isize::MAX);
1924 assert_eq!(overview.repeated_baselines_deduped, usize::MAX);
1925 assert_eq!(overview.observed_file_read_replacements, usize::MAX);
1926 assert_eq!(overview.modeled_file_reads_avoided, usize::MAX);
1927 assert_eq!(overview.likely_file_reads_avoided, usize::MAX);
1928 }
1929
1930 #[test]
1931 fn usage_from_text_tracks_positive_and_negative_savings() {
1932 let positive = usage_from_text("s", "outline", None, None, "abcdefghijkl", "abcd");
1933 assert_eq!(positive.estimated_tokens_without_projectatlas, Some(3));
1934 assert_eq!(positive.estimated_tokens_with_projectatlas, Some(1));
1935 assert_eq!(positive.estimated_tokens_saved, Some(2));
1936
1937 let negative = usage_from_estimates("s", "overview", None, None, 1, 4);
1938 assert_eq!(negative.estimated_tokens_saved, Some(-3));
1939 }
1940
1941 #[test]
1942 fn huge_estimates_use_saturating_signed_delta() {
1943 let event = usage_from_estimates("s", "large-repo", None, None, usize::MAX, 0);
1944 assert_eq!(event.estimated_tokens_saved, Some(isize::MAX));
1945 }
1946
1947 #[test]
1948 fn overview_recomputes_saved_from_aggregate_without_and_with() {
1949 let mut first = usage_from_estimates("s", "a", None, None, 20, 50);
1950 first.estimated_tokens_saved = Some(999);
1951 let mut second = usage_from_estimates("s", "b", None, None, 0, 10);
1952 second.estimated_tokens_saved = Some(999);
1953 let overview = TokenOverview::from_events(&[first, second]);
1954
1955 assert_eq!(overview.estimate_kind, TOKEN_ESTIMATE_KIND);
1956 assert_eq!(overview.estimator, TOKEN_ESTIMATOR);
1957 assert_eq!(overview.estimate_scope, TOKEN_ESTIMATE_SCOPE);
1958 assert_eq!(overview.calls, 2);
1959 assert_eq!(overview.estimated_without_projectatlas, 20);
1960 assert_eq!(overview.estimated_with_projectatlas, 60);
1961 assert_eq!(overview.estimated_saved, -40);
1962 assert_eq!(overview.savings_rate, Some(-2.0));
1963 }
1964
1965 #[test]
1966 fn overview_keeps_source_compression_and_navigation_buckets_separate() {
1967 let overview = TokenOverview::from_events(&[
1968 usage_from_text("s", "summary", None, None, "abcdefghijkl", "abcd"),
1969 usage_from_estimates("s", "search", None, None, 100, 20),
1970 ]);
1971
1972 assert_eq!(overview.calls, 2);
1973 assert_eq!(overview.buckets.len(), 2);
1974 assert_eq!(
1975 overview.buckets[0].token_savings_bucket,
1976 TOKEN_BUCKET_FULL_FILE_COMPRESSION
1977 );
1978 assert_eq!(
1979 overview.buckets[1].token_savings_bucket,
1980 TOKEN_BUCKET_NAVIGATION_AVOIDANCE
1981 );
1982 assert_eq!(overview.observed_file_read_replacements, 1);
1983 assert_eq!(overview.modeled_file_reads_avoided, 1);
1984 assert_eq!(overview.likely_file_reads_avoided, 2);
1985 assert_eq!(
1986 overview.read_avoidance_confidence,
1987 READ_AVOIDANCE_CONFIDENCE_MODELED
1988 );
1989 assert_eq!(overview.read_avoidance_scope, READ_AVOIDANCE_SCOPE);
1990 }
1991
1992 #[test]
1993 fn observed_only_overview_reports_observed_read_avoidance_confidence() {
1994 let overview = TokenOverview::from_events(&[usage_from_text(
1995 "s",
1996 "summary",
1997 None,
1998 None,
1999 "abcdefghijkl",
2000 "abcd",
2001 )]);
2002
2003 assert_eq!(overview.observed_file_read_replacements, 1);
2004 assert_eq!(overview.modeled_file_reads_avoided, 0);
2005 assert_eq!(overview.likely_file_reads_avoided, 1);
2006 assert_eq!(
2007 overview.read_avoidance_confidence,
2008 READ_AVOIDANCE_CONFIDENCE_OBSERVED
2009 );
2010 }
2011
2012 #[test]
2013 fn bucket_overview_does_not_infer_read_avoidance_without_raw_events() {
2014 let event_overview = TokenOverview::from_events(&[
2015 usage_from_text("s", "summary", None, None, "abcdefghijkl", "abcd"),
2016 usage_from_estimates("s", "search", None, None, 100, 20),
2017 ]);
2018 let bucket_overview = TokenOverview::from_buckets(event_overview.buckets);
2019
2020 assert_eq!(bucket_overview.observed_file_read_replacements, 0);
2021 assert_eq!(bucket_overview.modeled_file_reads_avoided, 0);
2022 assert_eq!(bucket_overview.likely_file_reads_avoided, 0);
2023 assert_eq!(
2024 bucket_overview.read_avoidance_confidence,
2025 READ_AVOIDANCE_CONFIDENCE_NOT_RECORDED
2026 );
2027 }
2028
2029 #[test]
2030 fn non_file_read_navigation_events_do_not_increment_read_avoidance() {
2031 let overview = TokenOverview::from_events(&[
2032 usage_from_estimates("s", "overview", None, None, 100, 20),
2033 usage_from_estimates("s", "folders", None, None, 100, 20),
2034 usage_from_estimates("s", "files", None, None, 100, 20),
2035 usage_from_estimates("s", "mcp.atlas_health", None, None, 100, 20),
2036 usage_from_estimates("s", "mcp.atlas_purpose_queue", None, None, 100, 20),
2037 ]);
2038
2039 assert_eq!(overview.modeled_file_reads_avoided, 0);
2040 assert_eq!(overview.likely_file_reads_avoided, 0);
2041 }
2042
2043 #[test]
2044 fn zero_baseline_events_do_not_increment_read_avoidance() {
2045 let overview = TokenOverview::from_events(&[
2046 usage_from_estimates("s", "search", None, None, 0, 20),
2047 usage_from_text("s", "summary", None, None, "", "summary"),
2048 ]);
2049
2050 assert_eq!(overview.observed_file_read_replacements, 0);
2051 assert_eq!(overview.modeled_file_reads_avoided, 0);
2052 assert_eq!(overview.likely_file_reads_avoided, 0);
2053 }
2054
2055 #[test]
2056 fn overview_dedupes_repeated_modeled_baselines_without_hiding_measured_savings() {
2057 let overview = TokenOverview::from_events(&[
2058 usage_from_text(
2059 "s",
2060 "summary",
2061 Some("src/lib.rs".to_string()),
2062 None,
2063 "abcdabcd",
2064 "ab",
2065 ),
2066 usage_from_estimates("s", "search", None, Some("token".to_string()), 400, 40),
2067 usage_from_estimates("s", "search", None, Some("token".to_string()), 400, 30),
2068 usage_from_estimates("s", "search", None, Some("token".to_string()), 400, 20),
2069 ]);
2070
2071 assert_eq!(overview.estimated_saved, 1111);
2072 assert_eq!(overview.legacy_gross_estimated_saved, 1111);
2073 assert_eq!(overview.measured_tokens_saved, 1);
2074 assert_eq!(overview.gross_modeled_tokens_avoided, 1110);
2075 assert_eq!(overview.deduped_modeled_tokens_avoided, 310);
2076 assert_eq!(overview.tokens_avoided, 311);
2077 assert_eq!(overview.repeated_baselines_deduped, 2);
2078 assert_eq!(overview.observed_file_read_replacements, 1);
2079 assert_eq!(overview.modeled_file_reads_avoided, 3);
2080 assert_eq!(overview.likely_file_reads_avoided, 4);
2081 }
2082}