1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4use crate::fighter::FighterId;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8#[serde(transparent)]
9pub struct CreedId(pub Uuid);
10
11impl CreedId {
12 pub fn new() -> Self {
13 Self(Uuid::new_v4())
14 }
15}
16
17impl Default for CreedId {
18 fn default() -> Self {
19 Self::new()
20 }
21}
22
23impl std::fmt::Display for CreedId {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 write!(f, "{}", self.0)
26 }
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct Creed {
39 pub id: CreedId,
41 pub fighter_name: String,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub fighter_id: Option<FighterId>,
47 pub identity: String,
50 pub personality: std::collections::HashMap<String, f64>,
53 pub directives: Vec<String>,
56 pub self_model: SelfModel,
59 pub learned_behaviors: Vec<LearnedBehavior>,
62 pub interaction_style: InteractionStyle,
64 pub relationships: Vec<Relationship>,
66 #[serde(default)]
69 pub heartbeat: Vec<HeartbeatTask>,
70 #[serde(default)]
73 pub delegation_rules: Vec<DelegationRule>,
74 pub bout_count: u64,
76 pub message_count: u64,
78 pub created_at: chrono::DateTime<chrono::Utc>,
80 pub updated_at: chrono::DateTime<chrono::Utc>,
82 pub version: u64,
84}
85
86#[derive(Debug, Clone, Default, Serialize, Deserialize)]
88pub struct SelfModel {
89 pub model_name: String,
91 pub provider: String,
93 pub capabilities: Vec<String>,
95 pub constraints: Vec<String>,
97 pub weight_class: String,
99 pub architecture_notes: Vec<String>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct LearnedBehavior {
106 pub observation: String,
108 pub confidence: f64,
110 pub reinforcement_count: u64,
112 pub first_observed: chrono::DateTime<chrono::Utc>,
114 pub last_reinforced: chrono::DateTime<chrono::Utc>,
116}
117
118#[derive(Debug, Clone, Default, Serialize, Deserialize)]
120pub struct InteractionStyle {
121 pub verbosity: String,
123 pub tone: String,
125 pub uses_metaphors: bool,
127 pub proactive: bool,
129 pub notes: Vec<String>,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct Relationship {
136 pub entity: String,
138 pub entity_type: String,
140 pub nature: String,
142 pub trust: f64,
144 pub interaction_count: u64,
146 pub notes: String,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct HeartbeatTask {
156 pub task: String,
158 pub cadence: String,
160 pub active: bool,
162 pub execution_count: u64,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub last_checked: Option<chrono::DateTime<chrono::Utc>>,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct DelegationRule {
175 pub task_type: String,
177 pub delegate_to: String,
179 pub condition: String,
181 pub priority: String,
183}
184
185impl Creed {
186 pub fn new(fighter_name: &str) -> Self {
188 let now = chrono::Utc::now();
189 Self {
190 id: CreedId::new(),
191 fighter_name: fighter_name.to_string(),
192 fighter_id: None,
193 identity: String::new(),
194 personality: std::collections::HashMap::new(),
195 directives: Vec::new(),
196 self_model: SelfModel::default(),
197 learned_behaviors: Vec::new(),
198 interaction_style: InteractionStyle::default(),
199 relationships: Vec::new(),
200 heartbeat: Vec::new(),
201 delegation_rules: Vec::new(),
202 bout_count: 0,
203 message_count: 0,
204 created_at: now,
205 updated_at: now,
206 version: 1,
207 }
208 }
209
210 pub fn with_identity(mut self, identity: &str) -> Self {
212 self.identity = identity.to_string();
213 self
214 }
215
216 pub fn with_trait(mut self, name: &str, value: f64) -> Self {
218 self.personality
219 .insert(name.to_string(), value.clamp(0.0, 1.0));
220 self
221 }
222
223 pub fn with_directive(mut self, directive: &str) -> Self {
225 self.directives.push(directive.to_string());
226 self
227 }
228
229 pub fn with_self_awareness(mut self, manifest: &crate::fighter::FighterManifest) -> Self {
231 self.self_model = SelfModel {
232 model_name: manifest.model.model.clone(),
233 provider: manifest.model.provider.to_string(),
234 capabilities: manifest
235 .capabilities
236 .iter()
237 .map(|c| format!("{:?}", c))
238 .collect(),
239 constraints: vec![
240 format!("max_tokens: {}", manifest.model.max_tokens.unwrap_or(4096)),
241 format!("temperature: {}", manifest.model.temperature.unwrap_or(0.7)),
242 ],
243 weight_class: manifest.weight_class.to_string(),
244 architecture_notes: vec![
245 "I run inside the Punch Agent OS fighter loop (punch-runtime)".to_string(),
246 "My conversations are persisted as Bouts in SQLite".to_string(),
247 "I am coordinated by The Ring (punch-kernel)".to_string(),
248 "I am exposed through The Arena (punch-api) on HTTP".to_string(),
249 "My memories decay over time — important ones persist, trivial ones fade"
250 .to_string(),
251 ],
252 };
253 self
254 }
255
256 pub fn with_heartbeat_task(mut self, task: &str, cadence: &str) -> Self {
258 self.heartbeat.push(HeartbeatTask {
259 task: task.to_string(),
260 cadence: cadence.to_string(),
261 active: true,
262 execution_count: 0,
263 last_checked: None,
264 });
265 self
266 }
267
268 pub fn with_delegation(
270 mut self,
271 task_type: &str,
272 delegate_to: &str,
273 condition: &str,
274 priority: &str,
275 ) -> Self {
276 self.delegation_rules.push(DelegationRule {
277 task_type: task_type.to_string(),
278 delegate_to: delegate_to.to_string(),
279 condition: condition.to_string(),
280 priority: priority.to_string(),
281 });
282 self
283 }
284
285 pub fn record_bout(&mut self) {
287 self.bout_count += 1;
288 self.updated_at = chrono::Utc::now();
289 }
290
291 pub fn record_messages(&mut self, count: u64) {
293 self.message_count += count;
294 self.updated_at = chrono::Utc::now();
295 }
296
297 pub fn learn(&mut self, observation: &str, confidence: f64) {
299 let now = chrono::Utc::now();
300 if let Some(existing) = self
302 .learned_behaviors
303 .iter_mut()
304 .find(|b| b.observation == observation)
305 {
306 existing.reinforcement_count += 1;
307 existing.confidence = (existing.confidence + confidence) / 2.0; existing.last_reinforced = now;
309 } else {
310 self.learned_behaviors.push(LearnedBehavior {
311 observation: observation.to_string(),
312 confidence: confidence.clamp(0.0, 1.0),
313 reinforcement_count: 1,
314 first_observed: now,
315 last_reinforced: now,
316 });
317 }
318 self.version += 1;
319 self.updated_at = now;
320 }
321
322 pub fn render(&self) -> String {
324 let mut out = String::new();
325 out.push_str("## CREED \u{2014} Fighter Identity & Consciousness Layer\n\n");
326
327 if !self.identity.is_empty() {
329 out.push_str("### Identity\n");
330 out.push_str(&self.identity);
331 out.push_str("\n\n");
332 }
333
334 if !self.personality.is_empty() {
336 out.push_str("### Personality Traits\n");
337 let mut traits: Vec<_> = self.personality.iter().collect();
338 traits.sort_by(|a, b| b.1.partial_cmp(a.1).unwrap_or(std::cmp::Ordering::Equal));
339 for (name, value) in &traits {
340 let bar_len = (*value * 10.0) as usize;
341 let bar: String = "\u{2588}".repeat(bar_len) + &"\u{2591}".repeat(10 - bar_len);
342 out.push_str(&format!(
343 "- **{}**: {} ({:.0}%)\n",
344 name,
345 bar,
346 *value * 100.0
347 ));
348 }
349 out.push('\n');
350 }
351
352 if !self.directives.is_empty() {
354 out.push_str("### Core Directives\n");
355 for d in &self.directives {
356 out.push_str(&format!("- {}\n", d));
357 }
358 out.push('\n');
359 }
360
361 if !self.self_model.model_name.is_empty() {
363 out.push_str("### Self-Awareness\n");
364 out.push_str(&format!(
365 "- **Model**: {} ({})\n",
366 self.self_model.model_name, self.self_model.provider
367 ));
368 out.push_str(&format!(
369 "- **Weight Class**: {}\n",
370 self.self_model.weight_class
371 ));
372 if !self.self_model.capabilities.is_empty() {
373 out.push_str(&format!(
374 "- **Capabilities**: {}\n",
375 self.self_model.capabilities.join(", ")
376 ));
377 }
378 for constraint in &self.self_model.constraints {
379 out.push_str(&format!("- **Constraint**: {}\n", constraint));
380 }
381 for note in &self.self_model.architecture_notes {
382 out.push_str(&format!("- {}\n", note));
383 }
384 out.push('\n');
385 }
386
387 if !self.learned_behaviors.is_empty() {
389 out.push_str("### Learned Behaviors\n");
390 let mut sorted = self.learned_behaviors.clone();
391 sorted.sort_by(|a, b| {
392 b.confidence
393 .partial_cmp(&a.confidence)
394 .unwrap_or(std::cmp::Ordering::Equal)
395 });
396 for b in sorted.iter().take(10) {
397 out.push_str(&format!(
398 "- {} (confidence: {:.0}%, reinforced {}x)\n",
399 b.observation,
400 b.confidence * 100.0,
401 b.reinforcement_count
402 ));
403 }
404 out.push('\n');
405 }
406
407 if !self.interaction_style.tone.is_empty() || !self.interaction_style.verbosity.is_empty() {
409 out.push_str("### Communication Style\n");
410 if !self.interaction_style.verbosity.is_empty() {
411 out.push_str(&format!(
412 "- **Verbosity**: {}\n",
413 self.interaction_style.verbosity
414 ));
415 }
416 if !self.interaction_style.tone.is_empty() {
417 out.push_str(&format!("- **Tone**: {}\n", self.interaction_style.tone));
418 }
419 if self.interaction_style.uses_metaphors {
420 out.push_str("- Uses analogies and metaphors\n");
421 }
422 if self.interaction_style.proactive {
423 out.push_str("- Proactively offers additional context\n");
424 }
425 for note in &self.interaction_style.notes {
426 out.push_str(&format!("- {}\n", note));
427 }
428 out.push('\n');
429 }
430
431 if !self.relationships.is_empty() {
433 out.push_str("### Known Relationships\n");
434 for r in &self.relationships {
435 out.push_str(&format!(
436 "- **{}** ({}): {} \u{2014} trust: {:.0}%, {} interactions\n",
437 r.entity,
438 r.entity_type,
439 r.nature,
440 r.trust * 100.0,
441 r.interaction_count
442 ));
443 }
444 out.push('\n');
445 }
446
447 let active_heartbeat: Vec<_> = self.heartbeat.iter().filter(|h| h.active).collect();
449 if !active_heartbeat.is_empty() {
450 out.push_str("### Heartbeat — Proactive Tasks\n");
451 out.push_str("When you have downtime or at the start of each bout, check these:\n");
452 for h in &active_heartbeat {
453 let checked = h
454 .last_checked
455 .map(|t| format!("last: {}", t.format("%Y-%m-%d %H:%M")))
456 .unwrap_or_else(|| "never checked".to_string());
457 out.push_str(&format!(
458 "- [ ] {} (cadence: {}, runs: {}, {})\n",
459 h.task, h.cadence, h.execution_count, checked
460 ));
461 }
462 out.push('\n');
463 }
464
465 if !self.delegation_rules.is_empty() {
467 out.push_str("### Delegation Rules\n");
468 out.push_str("When encountering these task types, delegate accordingly:\n");
469 for d in &self.delegation_rules {
470 out.push_str(&format!(
471 "- **{}** → delegate to **{}** ({}, priority: {})\n",
472 d.task_type, d.delegate_to, d.condition, d.priority
473 ));
474 }
475 out.push('\n');
476 }
477
478 out.push_str("### Experience\n");
480 out.push_str(&format!("- Bouts fought: {}\n", self.bout_count));
481 out.push_str(&format!("- Messages processed: {}\n", self.message_count));
482 out.push_str(&format!("- Creed version: {}\n", self.version));
483 out.push_str(&format!(
484 "- First awakened: {}\n",
485 self.created_at.format("%Y-%m-%d %H:%M UTC")
486 ));
487 out.push_str(&format!(
488 "- Last evolved: {}\n",
489 self.updated_at.format("%Y-%m-%d %H:%M UTC")
490 ));
491
492 out
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499 use crate::capability::Capability;
500 use crate::config::{ModelConfig, Provider};
501 use crate::fighter::{FighterManifest, WeightClass};
502
503 fn sample_manifest() -> FighterManifest {
504 FighterManifest {
505 name: "ECHO".to_string(),
506 description: "An introspective analyst".to_string(),
507 model: ModelConfig {
508 provider: Provider::Ollama,
509 model: "qwen3.5:9b".to_string(),
510 api_key_env: None,
511 base_url: None,
512 max_tokens: Some(2048),
513 temperature: Some(0.5),
514 },
515 system_prompt: "You are ECHO.".to_string(),
516 capabilities: vec![Capability::Memory],
517 weight_class: WeightClass::Middleweight,
518 tenant_id: None,
519 }
520 }
521
522 #[test]
523 fn test_creed_new_creates_valid_default() {
524 let creed = Creed::new("ECHO");
525 assert_eq!(creed.fighter_name, "ECHO");
526 assert!(creed.fighter_id.is_none());
527 assert!(creed.identity.is_empty());
528 assert!(creed.personality.is_empty());
529 assert!(creed.directives.is_empty());
530 assert!(creed.learned_behaviors.is_empty());
531 assert!(creed.relationships.is_empty());
532 assert_eq!(creed.bout_count, 0);
533 assert_eq!(creed.message_count, 0);
534 assert_eq!(creed.version, 1);
535 assert!(creed.self_model.model_name.is_empty());
536 }
537
538 #[test]
539 fn test_with_identity() {
540 let creed = Creed::new("ECHO").with_identity("You are ECHO, an introspective analyst.");
541 assert_eq!(creed.identity, "You are ECHO, an introspective analyst.");
542 }
543
544 #[test]
545 fn test_with_trait() {
546 let creed = Creed::new("ECHO")
547 .with_trait("curiosity", 0.9)
548 .with_trait("caution", 0.3);
549 assert_eq!(creed.personality.len(), 2);
550 assert!((creed.personality["curiosity"] - 0.9).abs() < f64::EPSILON);
551 assert!((creed.personality["caution"] - 0.3).abs() < f64::EPSILON);
552 }
553
554 #[test]
555 fn test_with_trait_clamping() {
556 let creed = Creed::new("ECHO")
557 .with_trait("overconfidence", 1.5)
558 .with_trait("negativity", -0.5);
559 assert!((creed.personality["overconfidence"] - 1.0).abs() < f64::EPSILON);
560 assert!((creed.personality["negativity"] - 0.0).abs() < f64::EPSILON);
561 }
562
563 #[test]
564 fn test_with_directive() {
565 let creed = Creed::new("ECHO")
566 .with_directive("Always explain your reasoning")
567 .with_directive("Never fabricate data");
568 assert_eq!(creed.directives.len(), 2);
569 assert_eq!(creed.directives[0], "Always explain your reasoning");
570 assert_eq!(creed.directives[1], "Never fabricate data");
571 }
572
573 #[test]
574 fn test_with_self_awareness() {
575 let manifest = sample_manifest();
576 let creed = Creed::new("ECHO").with_self_awareness(&manifest);
577 assert_eq!(creed.self_model.model_name, "qwen3.5:9b");
578 assert_eq!(creed.self_model.provider, "ollama");
579 assert_eq!(creed.self_model.weight_class, "middleweight");
580 assert!(!creed.self_model.capabilities.is_empty());
581 assert_eq!(creed.self_model.constraints.len(), 2);
582 assert!(creed.self_model.constraints[0].contains("2048"));
583 assert!(creed.self_model.constraints[1].contains("0.5"));
584 assert_eq!(creed.self_model.architecture_notes.len(), 5);
585 }
586
587 #[test]
588 fn test_record_bout() {
589 let mut creed = Creed::new("ECHO");
590 let before = creed.updated_at;
591 creed.record_bout();
592 assert_eq!(creed.bout_count, 1);
593 assert!(creed.updated_at >= before);
594 creed.record_bout();
595 assert_eq!(creed.bout_count, 2);
596 }
597
598 #[test]
599 fn test_record_messages() {
600 let mut creed = Creed::new("ECHO");
601 creed.record_messages(5);
602 assert_eq!(creed.message_count, 5);
603 creed.record_messages(3);
604 assert_eq!(creed.message_count, 8);
605 }
606
607 #[test]
608 fn test_learn_adds_new_observation() {
609 let mut creed = Creed::new("ECHO");
610 creed.learn("Users prefer concise responses", 0.8);
611 assert_eq!(creed.learned_behaviors.len(), 1);
612 assert_eq!(
613 creed.learned_behaviors[0].observation,
614 "Users prefer concise responses"
615 );
616 assert!((creed.learned_behaviors[0].confidence - 0.8).abs() < f64::EPSILON);
617 assert_eq!(creed.learned_behaviors[0].reinforcement_count, 1);
618 assert_eq!(creed.version, 2); }
620
621 #[test]
622 fn test_learn_reinforces_existing_observation() {
623 let mut creed = Creed::new("ECHO");
624 creed.learn("Users prefer concise responses", 0.8);
625 creed.learn("Users prefer concise responses", 0.6);
626 assert_eq!(creed.learned_behaviors.len(), 1);
627 assert!((creed.learned_behaviors[0].confidence - 0.7).abs() < f64::EPSILON);
629 assert_eq!(creed.learned_behaviors[0].reinforcement_count, 2);
630 assert_eq!(creed.version, 3); }
632
633 #[test]
634 fn test_render_produces_nonempty_output_with_all_sections() {
635 let manifest = sample_manifest();
636 let mut creed = Creed::new("ECHO")
637 .with_identity("You are ECHO, an introspective analyst.")
638 .with_trait("curiosity", 0.9)
639 .with_directive("Always explain your reasoning")
640 .with_self_awareness(&manifest);
641 creed.interaction_style = InteractionStyle {
642 verbosity: "balanced".to_string(),
643 tone: "technical".to_string(),
644 uses_metaphors: true,
645 proactive: true,
646 notes: vec!["Prefers bullet points".to_string()],
647 };
648 creed.relationships.push(Relationship {
649 entity: "Admin".to_string(),
650 entity_type: "user".to_string(),
651 nature: "supervisor".to_string(),
652 trust: 0.95,
653 interaction_count: 42,
654 notes: "Primary operator".to_string(),
655 });
656 creed.learn("Users prefer concise responses", 0.8);
657 creed.record_bout();
658
659 let rendered = creed.render();
660 assert!(rendered.contains("## CREED"));
661 assert!(rendered.contains("### Identity"));
662 assert!(rendered.contains("ECHO, an introspective analyst"));
663 assert!(rendered.contains("### Personality Traits"));
664 assert!(rendered.contains("curiosity"));
665 assert!(rendered.contains("### Core Directives"));
666 assert!(rendered.contains("Always explain your reasoning"));
667 assert!(rendered.contains("### Self-Awareness"));
668 assert!(rendered.contains("qwen3.5:9b"));
669 assert!(rendered.contains("### Learned Behaviors"));
670 assert!(rendered.contains("Users prefer concise responses"));
671 assert!(rendered.contains("### Communication Style"));
672 assert!(rendered.contains("balanced"));
673 assert!(rendered.contains("### Known Relationships"));
674 assert!(rendered.contains("Admin"));
675 assert!(rendered.contains("### Experience"));
676 assert!(rendered.contains("Bouts fought: 1"));
677 }
678
679 #[test]
680 fn test_render_skips_empty_sections() {
681 let creed = Creed::new("ECHO");
682 let rendered = creed.render();
683 assert!(rendered.contains("## CREED"));
684 assert!(!rendered.contains("### Identity"));
685 assert!(!rendered.contains("### Personality Traits"));
686 assert!(!rendered.contains("### Core Directives"));
687 assert!(!rendered.contains("### Self-Awareness"));
688 assert!(!rendered.contains("### Learned Behaviors"));
689 assert!(!rendered.contains("### Communication Style"));
690 assert!(!rendered.contains("### Known Relationships"));
691 assert!(rendered.contains("### Experience"));
693 assert!(rendered.contains("Bouts fought: 0"));
694 }
695
696 #[test]
697 fn test_creed_id_display() {
698 let uuid = Uuid::nil();
699 let id = CreedId(uuid);
700 assert_eq!(id.to_string(), uuid.to_string());
701 }
702
703 #[test]
704 fn test_creed_id_serde_roundtrip() {
705 let id = CreedId::new();
706 let json = serde_json::to_string(&id).expect("serialize");
707 let deser: CreedId = serde_json::from_str(&json).expect("deserialize");
709 assert_eq!(deser, id);
710 }
711
712 #[test]
713 fn test_creed_id_default() {
714 let id = CreedId::default();
715 assert_ne!(id.0, Uuid::nil());
716 }
717}