Skip to main content

punch_types/
fighter.rs

1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4use crate::capability::Capability;
5use crate::config::ModelConfig;
6use crate::tenant::TenantId;
7
8/// Unique identifier for a Fighter (conversational agent).
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(transparent)]
11pub struct FighterId(pub Uuid);
12
13impl FighterId {
14    pub fn new() -> Self {
15        Self(Uuid::new_v4())
16    }
17}
18
19impl Default for FighterId {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl std::fmt::Display for FighterId {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        write!(f, "{}", self.0)
28    }
29}
30
31/// Model tier determining the weight class of a Fighter.
32///
33/// Higher weight classes use more powerful (and expensive) models.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum WeightClass {
37    /// Lightweight, fast models (e.g. Haiku, GPT-4o-mini)
38    Featherweight,
39    /// Balanced models (e.g. Sonnet, GPT-4o)
40    Middleweight,
41    /// High-capability models (e.g. Opus, o1)
42    Heavyweight,
43    /// Top-tier, unrestricted models
44    Champion,
45}
46
47impl std::fmt::Display for WeightClass {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        match self {
50            Self::Featherweight => write!(f, "featherweight"),
51            Self::Middleweight => write!(f, "middleweight"),
52            Self::Heavyweight => write!(f, "heavyweight"),
53            Self::Champion => write!(f, "champion"),
54        }
55    }
56}
57
58/// The manifest describing a Fighter's configuration and identity.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct FighterManifest {
61    /// Human-readable name for this Fighter.
62    pub name: String,
63    /// Description of the Fighter's purpose and specialty.
64    pub description: String,
65    /// Model configuration for this Fighter.
66    pub model: ModelConfig,
67    /// System prompt that shapes the Fighter's behavior.
68    pub system_prompt: String,
69    /// Capabilities granted to this Fighter.
70    pub capabilities: Vec<Capability>,
71    /// The model tier / weight class.
72    pub weight_class: WeightClass,
73    /// The tenant that owns this fighter. None for single-tenant / backward compat.
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub tenant_id: Option<TenantId>,
76}
77
78/// Current operational status of a Fighter.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
80#[serde(rename_all = "snake_case")]
81pub enum FighterStatus {
82    /// Ready and waiting for a bout.
83    Idle,
84    /// Actively engaged in a conversation / task.
85    Fighting,
86    /// Temporarily paused (e.g. rate-limited).
87    Resting,
88    /// Encountered a fatal error and is no longer operational.
89    KnockedOut,
90    /// Undergoing fine-tuning or calibration.
91    Training,
92}
93
94impl std::fmt::Display for FighterStatus {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        match self {
97            Self::Idle => write!(f, "idle"),
98            Self::Fighting => write!(f, "fighting"),
99            Self::Resting => write!(f, "resting"),
100            Self::KnockedOut => write!(f, "knocked_out"),
101            Self::Training => write!(f, "training"),
102        }
103    }
104}
105
106/// Runtime statistics for a Fighter.
107#[derive(Debug, Clone, Default, Serialize, Deserialize)]
108pub struct FighterStats {
109    /// Total messages sent by this Fighter.
110    pub messages_sent: u64,
111    /// Total tokens consumed.
112    pub tokens_used: u64,
113    /// Number of bouts won (tasks completed successfully).
114    pub bouts_won: u64,
115    /// Number of knockouts (unrecoverable errors).
116    pub knockouts: u64,
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::config::{ModelConfig, Provider};
123
124    #[test]
125    fn test_fighter_id_display() {
126        let uuid = Uuid::nil();
127        let id = FighterId(uuid);
128        assert_eq!(id.to_string(), uuid.to_string());
129    }
130
131    #[test]
132    fn test_fighter_id_new_is_unique() {
133        let id1 = FighterId::new();
134        let id2 = FighterId::new();
135        assert_ne!(id1, id2);
136    }
137
138    #[test]
139    fn test_fighter_id_default() {
140        let id = FighterId::default();
141        assert_ne!(id.0, Uuid::nil());
142    }
143
144    #[test]
145    fn test_fighter_id_serde_transparent() {
146        let uuid = Uuid::new_v4();
147        let id = FighterId(uuid);
148        let json = serde_json::to_string(&id).expect("serialize");
149        // transparent means it serializes as just the UUID string
150        assert_eq!(json, format!("\"{}\"", uuid));
151        let deser: FighterId = serde_json::from_str(&json).expect("deserialize");
152        assert_eq!(deser, id);
153    }
154
155    #[test]
156    fn test_fighter_id_copy_clone() {
157        let id = FighterId::new();
158        let copied = id; // Copy
159        let cloned = id.clone();
160        assert_eq!(id, copied);
161        assert_eq!(id, cloned);
162    }
163
164    #[test]
165    fn test_fighter_id_hash() {
166        let id = FighterId::new();
167        let mut set = std::collections::HashSet::new();
168        set.insert(id);
169        set.insert(id); // duplicate
170        assert_eq!(set.len(), 1);
171    }
172
173    #[test]
174    fn test_weight_class_display() {
175        assert_eq!(WeightClass::Featherweight.to_string(), "featherweight");
176        assert_eq!(WeightClass::Middleweight.to_string(), "middleweight");
177        assert_eq!(WeightClass::Heavyweight.to_string(), "heavyweight");
178        assert_eq!(WeightClass::Champion.to_string(), "champion");
179    }
180
181    #[test]
182    fn test_weight_class_serde_roundtrip() {
183        let classes = vec![
184            WeightClass::Featherweight,
185            WeightClass::Middleweight,
186            WeightClass::Heavyweight,
187            WeightClass::Champion,
188        ];
189        for wc in &classes {
190            let json = serde_json::to_string(wc).expect("serialize");
191            let deser: WeightClass = serde_json::from_str(&json).expect("deserialize");
192            assert_eq!(&deser, wc);
193        }
194    }
195
196    #[test]
197    fn test_weight_class_serde_values() {
198        assert_eq!(
199            serde_json::to_string(&WeightClass::Featherweight).unwrap(),
200            "\"featherweight\""
201        );
202        assert_eq!(
203            serde_json::to_string(&WeightClass::Champion).unwrap(),
204            "\"champion\""
205        );
206    }
207
208    #[test]
209    fn test_fighter_status_display() {
210        assert_eq!(FighterStatus::Idle.to_string(), "idle");
211        assert_eq!(FighterStatus::Fighting.to_string(), "fighting");
212        assert_eq!(FighterStatus::Resting.to_string(), "resting");
213        assert_eq!(FighterStatus::KnockedOut.to_string(), "knocked_out");
214        assert_eq!(FighterStatus::Training.to_string(), "training");
215    }
216
217    #[test]
218    fn test_fighter_status_serde_roundtrip() {
219        let statuses = vec![
220            FighterStatus::Idle,
221            FighterStatus::Fighting,
222            FighterStatus::Resting,
223            FighterStatus::KnockedOut,
224            FighterStatus::Training,
225        ];
226        for status in &statuses {
227            let json = serde_json::to_string(status).expect("serialize");
228            let deser: FighterStatus = serde_json::from_str(&json).expect("deserialize");
229            assert_eq!(&deser, status);
230        }
231    }
232
233    #[test]
234    fn test_fighter_status_equality() {
235        assert_eq!(FighterStatus::Idle, FighterStatus::Idle);
236        assert_ne!(FighterStatus::Idle, FighterStatus::Fighting);
237    }
238
239    #[test]
240    fn test_fighter_stats_default() {
241        let stats = FighterStats::default();
242        assert_eq!(stats.messages_sent, 0);
243        assert_eq!(stats.tokens_used, 0);
244        assert_eq!(stats.bouts_won, 0);
245        assert_eq!(stats.knockouts, 0);
246    }
247
248    #[test]
249    fn test_fighter_stats_serde_roundtrip() {
250        let stats = FighterStats {
251            messages_sent: 100,
252            tokens_used: 50000,
253            bouts_won: 10,
254            knockouts: 2,
255        };
256        let json = serde_json::to_string(&stats).expect("serialize");
257        let deser: FighterStats = serde_json::from_str(&json).expect("deserialize");
258        assert_eq!(deser.messages_sent, 100);
259        assert_eq!(deser.tokens_used, 50000);
260        assert_eq!(deser.bouts_won, 10);
261        assert_eq!(deser.knockouts, 2);
262    }
263
264    #[test]
265    fn test_fighter_manifest_serde() {
266        let manifest = FighterManifest {
267            name: "TestFighter".to_string(),
268            description: "A test fighter".to_string(),
269            model: ModelConfig {
270                provider: Provider::Anthropic,
271                model: "claude-sonnet-4-20250514".to_string(),
272                api_key_env: None,
273                base_url: None,
274                max_tokens: None,
275                temperature: None,
276            },
277            system_prompt: "You are helpful".to_string(),
278            capabilities: vec![Capability::Memory],
279            weight_class: WeightClass::Middleweight,
280            tenant_id: None,
281        };
282        let json = serde_json::to_string(&manifest).expect("serialize");
283        let deser: FighterManifest = serde_json::from_str(&json).expect("deserialize");
284        assert_eq!(deser.name, "TestFighter");
285        assert_eq!(deser.weight_class, WeightClass::Middleweight);
286        assert_eq!(deser.capabilities.len(), 1);
287        assert!(deser.tenant_id.is_none());
288    }
289}