1use aios_protocol::event::EventKind;
8use serde::{Deserialize, Serialize};
9use serde_json::json;
10
11use crate::economic::{CostReason, EconomicMode};
12
13pub const AUTONOMIC_EVENT_PREFIX: &str = "autonomic.";
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(tag = "type", rename_all = "PascalCase")]
19pub enum AutonomicEvent {
20 CostCharged {
22 amount_micro_credits: i64,
23 reason: CostReason,
24 balance_after: i64,
25 },
26 EconomicModeChanged {
28 from: EconomicMode,
29 to: EconomicMode,
30 reason: String,
31 },
32 GatingDecision {
34 session_id: String,
35 rationale: Vec<String>,
36 economic_mode: EconomicMode,
37 },
38 CreditDeposited {
40 amount_micro_credits: i64,
41 source: String,
42 balance_after: i64,
43 },
44}
45
46impl AutonomicEvent {
47 pub fn into_event_kind(self) -> EventKind {
49 let (event_type, data) = match &self {
50 Self::CostCharged {
51 amount_micro_credits,
52 reason,
53 balance_after,
54 } => (
55 "autonomic.CostCharged",
56 json!({
57 "amount_micro_credits": amount_micro_credits,
58 "reason": reason,
59 "balance_after": balance_after,
60 }),
61 ),
62 Self::EconomicModeChanged { from, to, reason } => (
63 "autonomic.EconomicModeChanged",
64 json!({
65 "from": from,
66 "to": to,
67 "reason": reason,
68 }),
69 ),
70 Self::GatingDecision {
71 session_id,
72 rationale,
73 economic_mode,
74 } => (
75 "autonomic.GatingDecision",
76 json!({
77 "session_id": session_id,
78 "rationale": rationale,
79 "economic_mode": economic_mode,
80 }),
81 ),
82 Self::CreditDeposited {
83 amount_micro_credits,
84 source,
85 balance_after,
86 } => (
87 "autonomic.CreditDeposited",
88 json!({
89 "amount_micro_credits": amount_micro_credits,
90 "source": source,
91 "balance_after": balance_after,
92 }),
93 ),
94 };
95 EventKind::Custom {
96 event_type: event_type.to_owned(),
97 data,
98 }
99 }
100
101 pub fn is_autonomic_event(event_type: &str) -> bool {
103 event_type.starts_with(AUTONOMIC_EVENT_PREFIX)
104 }
105
106 pub fn from_custom(event_type: &str, data: &serde_json::Value) -> Option<Self> {
108 if !Self::is_autonomic_event(event_type) {
109 return None;
110 }
111
112 match event_type {
113 "autonomic.CostCharged" => {
114 let amount = data.get("amount_micro_credits")?.as_i64()?;
115 let reason: CostReason =
116 serde_json::from_value(data.get("reason")?.clone()).ok()?;
117 let balance = data.get("balance_after")?.as_i64()?;
118 Some(Self::CostCharged {
119 amount_micro_credits: amount,
120 reason,
121 balance_after: balance,
122 })
123 }
124 "autonomic.EconomicModeChanged" => {
125 let from: EconomicMode = serde_json::from_value(data.get("from")?.clone()).ok()?;
126 let to: EconomicMode = serde_json::from_value(data.get("to")?.clone()).ok()?;
127 let reason = data.get("reason")?.as_str()?.to_owned();
128 Some(Self::EconomicModeChanged { from, to, reason })
129 }
130 "autonomic.GatingDecision" => {
131 let session_id = data.get("session_id")?.as_str()?.to_owned();
132 let rationale: Vec<String> =
133 serde_json::from_value(data.get("rationale")?.clone()).ok()?;
134 let economic_mode: EconomicMode =
135 serde_json::from_value(data.get("economic_mode")?.clone()).ok()?;
136 Some(Self::GatingDecision {
137 session_id,
138 rationale,
139 economic_mode,
140 })
141 }
142 "autonomic.CreditDeposited" => {
143 let amount = data.get("amount_micro_credits")?.as_i64()?;
144 let source = data.get("source")?.as_str()?.to_owned();
145 let balance = data.get("balance_after")?.as_i64()?;
146 Some(Self::CreditDeposited {
147 amount_micro_credits: amount,
148 source,
149 balance_after: balance,
150 })
151 }
152 _ => None,
153 }
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn cost_charged_to_event_kind() {
163 let event = AutonomicEvent::CostCharged {
164 amount_micro_credits: 150,
165 reason: CostReason::ModelInference {
166 model: "claude-sonnet".into(),
167 prompt_tokens: 100,
168 completion_tokens: 50,
169 },
170 balance_after: 9_999_850,
171 };
172 let kind = event.into_event_kind();
173 if let EventKind::Custom { event_type, data } = &kind {
174 assert_eq!(event_type, "autonomic.CostCharged");
175 assert_eq!(data["amount_micro_credits"], 150);
176 assert_eq!(data["balance_after"], 9_999_850);
177 } else {
178 panic!("expected Custom variant");
179 }
180 }
181
182 #[test]
183 fn event_kind_roundtrip_through_custom() {
184 let event = AutonomicEvent::EconomicModeChanged {
185 from: EconomicMode::Sovereign,
186 to: EconomicMode::Conserving,
187 reason: "balance dropping".into(),
188 };
189 let kind = event.into_event_kind();
190
191 let json = serde_json::to_string(&kind).unwrap();
193 let back: EventKind = serde_json::from_str(&json).unwrap();
194
195 if let EventKind::Custom { event_type, data } = back {
196 assert_eq!(event_type, "autonomic.EconomicModeChanged");
197 let parsed = AutonomicEvent::from_custom(&event_type, &data).unwrap();
198 assert!(matches!(
199 parsed,
200 AutonomicEvent::EconomicModeChanged {
201 to: EconomicMode::Conserving,
202 ..
203 }
204 ));
205 } else {
206 panic!("expected Custom variant after roundtrip");
207 }
208 }
209
210 #[test]
211 fn is_autonomic_event_prefix() {
212 assert!(AutonomicEvent::is_autonomic_event("autonomic.CostCharged"));
213 assert!(AutonomicEvent::is_autonomic_event("autonomic.Anything"));
214 assert!(!AutonomicEvent::is_autonomic_event("other.Event"));
215 assert!(!AutonomicEvent::is_autonomic_event("CostCharged"));
216 }
217
218 #[test]
219 fn from_custom_returns_none_for_non_autonomic() {
220 let result = AutonomicEvent::from_custom("other.Event", &json!({}));
221 assert!(result.is_none());
222 }
223
224 #[test]
225 fn credit_deposited_roundtrip() {
226 let event = AutonomicEvent::CreditDeposited {
227 amount_micro_credits: 5_000_000,
228 source: "grant".into(),
229 balance_after: 15_000_000,
230 };
231 let kind = event.into_event_kind();
232 if let EventKind::Custom { event_type, data } = kind {
233 let parsed = AutonomicEvent::from_custom(&event_type, &data).unwrap();
234 assert!(matches!(
235 parsed,
236 AutonomicEvent::CreditDeposited {
237 amount_micro_credits: 5_000_000,
238 ..
239 }
240 ));
241 } else {
242 panic!("expected Custom");
243 }
244 }
245}