use aios_protocol::event::EventKind;
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::economic::{CostReason, EconomicMode};
pub const AUTONOMIC_EVENT_PREFIX: &str = "autonomic.";
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "PascalCase")]
pub enum AutonomicEvent {
CostCharged {
amount_micro_credits: i64,
reason: CostReason,
balance_after: i64,
},
EconomicModeChanged {
from: EconomicMode,
to: EconomicMode,
reason: String,
},
GatingDecision {
session_id: String,
rationale: Vec<String>,
economic_mode: EconomicMode,
},
CreditDeposited {
amount_micro_credits: i64,
source: String,
balance_after: i64,
},
}
impl AutonomicEvent {
pub fn into_event_kind(self) -> EventKind {
let (event_type, data) = match &self {
Self::CostCharged {
amount_micro_credits,
reason,
balance_after,
} => (
"autonomic.CostCharged",
json!({
"amount_micro_credits": amount_micro_credits,
"reason": reason,
"balance_after": balance_after,
}),
),
Self::EconomicModeChanged { from, to, reason } => (
"autonomic.EconomicModeChanged",
json!({
"from": from,
"to": to,
"reason": reason,
}),
),
Self::GatingDecision {
session_id,
rationale,
economic_mode,
} => (
"autonomic.GatingDecision",
json!({
"session_id": session_id,
"rationale": rationale,
"economic_mode": economic_mode,
}),
),
Self::CreditDeposited {
amount_micro_credits,
source,
balance_after,
} => (
"autonomic.CreditDeposited",
json!({
"amount_micro_credits": amount_micro_credits,
"source": source,
"balance_after": balance_after,
}),
),
};
EventKind::Custom {
event_type: event_type.to_owned(),
data,
}
}
pub fn is_autonomic_event(event_type: &str) -> bool {
event_type.starts_with(AUTONOMIC_EVENT_PREFIX)
}
pub fn from_custom(event_type: &str, data: &serde_json::Value) -> Option<Self> {
if !Self::is_autonomic_event(event_type) {
return None;
}
match event_type {
"autonomic.CostCharged" => {
let amount = data.get("amount_micro_credits")?.as_i64()?;
let reason: CostReason =
serde_json::from_value(data.get("reason")?.clone()).ok()?;
let balance = data.get("balance_after")?.as_i64()?;
Some(Self::CostCharged {
amount_micro_credits: amount,
reason,
balance_after: balance,
})
}
"autonomic.EconomicModeChanged" => {
let from: EconomicMode = serde_json::from_value(data.get("from")?.clone()).ok()?;
let to: EconomicMode = serde_json::from_value(data.get("to")?.clone()).ok()?;
let reason = data.get("reason")?.as_str()?.to_owned();
Some(Self::EconomicModeChanged { from, to, reason })
}
"autonomic.GatingDecision" => {
let session_id = data.get("session_id")?.as_str()?.to_owned();
let rationale: Vec<String> =
serde_json::from_value(data.get("rationale")?.clone()).ok()?;
let economic_mode: EconomicMode =
serde_json::from_value(data.get("economic_mode")?.clone()).ok()?;
Some(Self::GatingDecision {
session_id,
rationale,
economic_mode,
})
}
"autonomic.CreditDeposited" => {
let amount = data.get("amount_micro_credits")?.as_i64()?;
let source = data.get("source")?.as_str()?.to_owned();
let balance = data.get("balance_after")?.as_i64()?;
Some(Self::CreditDeposited {
amount_micro_credits: amount,
source,
balance_after: balance,
})
}
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cost_charged_to_event_kind() {
let event = AutonomicEvent::CostCharged {
amount_micro_credits: 150,
reason: CostReason::ModelInference {
model: "claude-sonnet".into(),
prompt_tokens: 100,
completion_tokens: 50,
},
balance_after: 9_999_850,
};
let kind = event.into_event_kind();
if let EventKind::Custom { event_type, data } = &kind {
assert_eq!(event_type, "autonomic.CostCharged");
assert_eq!(data["amount_micro_credits"], 150);
assert_eq!(data["balance_after"], 9_999_850);
} else {
panic!("expected Custom variant");
}
}
#[test]
fn event_kind_roundtrip_through_custom() {
let event = AutonomicEvent::EconomicModeChanged {
from: EconomicMode::Sovereign,
to: EconomicMode::Conserving,
reason: "balance dropping".into(),
};
let kind = event.into_event_kind();
let json = serde_json::to_string(&kind).unwrap();
let back: EventKind = serde_json::from_str(&json).unwrap();
if let EventKind::Custom { event_type, data } = back {
assert_eq!(event_type, "autonomic.EconomicModeChanged");
let parsed = AutonomicEvent::from_custom(&event_type, &data).unwrap();
assert!(matches!(
parsed,
AutonomicEvent::EconomicModeChanged {
to: EconomicMode::Conserving,
..
}
));
} else {
panic!("expected Custom variant after roundtrip");
}
}
#[test]
fn is_autonomic_event_prefix() {
assert!(AutonomicEvent::is_autonomic_event("autonomic.CostCharged"));
assert!(AutonomicEvent::is_autonomic_event("autonomic.Anything"));
assert!(!AutonomicEvent::is_autonomic_event("other.Event"));
assert!(!AutonomicEvent::is_autonomic_event("CostCharged"));
}
#[test]
fn from_custom_returns_none_for_non_autonomic() {
let result = AutonomicEvent::from_custom("other.Event", &json!({}));
assert!(result.is_none());
}
#[test]
fn credit_deposited_roundtrip() {
let event = AutonomicEvent::CreditDeposited {
amount_micro_credits: 5_000_000,
source: "grant".into(),
balance_after: 15_000_000,
};
let kind = event.into_event_kind();
if let EventKind::Custom { event_type, data } = kind {
let parsed = AutonomicEvent::from_custom(&event_type, &data).unwrap();
assert!(matches!(
parsed,
AutonomicEvent::CreditDeposited {
amount_micro_credits: 5_000_000,
..
}
));
} else {
panic!("expected Custom");
}
}
}