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 #[serde(default)]
77 pub preferences: std::collections::HashMap<String, String>,
78 pub bout_count: u64,
80 pub message_count: u64,
82 pub created_at: chrono::DateTime<chrono::Utc>,
84 pub updated_at: chrono::DateTime<chrono::Utc>,
86 pub version: u64,
88}
89
90#[derive(Debug, Clone, Default, Serialize, Deserialize)]
92pub struct SelfModel {
93 pub model_name: String,
95 pub provider: String,
97 pub capabilities: Vec<String>,
99 pub constraints: Vec<String>,
101 pub weight_class: String,
103 pub architecture_notes: Vec<String>,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct LearnedBehavior {
110 pub observation: String,
112 pub confidence: f64,
114 pub reinforcement_count: u64,
116 pub first_observed: chrono::DateTime<chrono::Utc>,
118 pub last_reinforced: chrono::DateTime<chrono::Utc>,
120}
121
122#[derive(Debug, Clone, Default, Serialize, Deserialize)]
124pub struct InteractionStyle {
125 pub verbosity: String,
127 pub tone: String,
129 pub uses_metaphors: bool,
131 pub proactive: bool,
133 pub notes: Vec<String>,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct Relationship {
140 pub entity: String,
142 pub entity_type: String,
144 pub nature: String,
146 pub trust: f64,
148 pub interaction_count: u64,
150 pub notes: String,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct HeartbeatTask {
160 pub task: String,
162 pub cadence: String,
164 pub active: bool,
166 pub execution_count: u64,
168 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub last_checked: Option<chrono::DateTime<chrono::Utc>>,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct DelegationRule {
179 pub task_type: String,
181 pub delegate_to: String,
183 pub condition: String,
185 pub priority: String,
187}
188
189impl Creed {
190 pub fn new(fighter_name: &str) -> Self {
192 let now = chrono::Utc::now();
193 Self {
194 id: CreedId::new(),
195 fighter_name: fighter_name.to_string(),
196 fighter_id: None,
197 identity: String::new(),
198 personality: std::collections::HashMap::new(),
199 directives: Vec::new(),
200 self_model: SelfModel::default(),
201 learned_behaviors: Vec::new(),
202 interaction_style: InteractionStyle::default(),
203 relationships: Vec::new(),
204 heartbeat: Vec::new(),
205 delegation_rules: Vec::new(),
206 preferences: std::collections::HashMap::new(),
207 bout_count: 0,
208 message_count: 0,
209 created_at: now,
210 updated_at: now,
211 version: 1,
212 }
213 }
214
215 pub fn with_identity(mut self, identity: &str) -> Self {
217 self.identity = identity.to_string();
218 self
219 }
220
221 pub fn with_trait(mut self, name: &str, value: f64) -> Self {
223 self.personality
224 .insert(name.to_string(), value.clamp(0.0, 1.0));
225 self
226 }
227
228 pub fn with_directive(mut self, directive: &str) -> Self {
230 self.directives.push(directive.to_string());
231 self
232 }
233
234 pub fn with_self_awareness(mut self, manifest: &crate::fighter::FighterManifest) -> Self {
236 self.self_model = SelfModel {
237 model_name: manifest.model.model.clone(),
238 provider: manifest.model.provider.to_string(),
239 capabilities: manifest
240 .capabilities
241 .iter()
242 .map(|c| format!("{:?}", c))
243 .collect(),
244 constraints: vec![
245 format!("max_tokens: {}", manifest.model.max_tokens.unwrap_or(4096)),
246 format!("temperature: {}", manifest.model.temperature.unwrap_or(0.7)),
247 ],
248 weight_class: manifest.weight_class.to_string(),
249 architecture_notes: vec![
250 "I run inside the Punch Agent OS fighter loop (punch-runtime)".to_string(),
251 "My conversations are persisted as Bouts in SQLite".to_string(),
252 "I am coordinated by The Ring (punch-kernel)".to_string(),
253 "I am exposed through The Arena (punch-api) on HTTP".to_string(),
254 "My memories decay over time — important ones persist, trivial ones fade"
255 .to_string(),
256 ],
257 };
258 self
259 }
260
261 pub fn with_heartbeat_task(mut self, task: &str, cadence: &str) -> Self {
263 self.heartbeat.push(HeartbeatTask {
264 task: task.to_string(),
265 cadence: cadence.to_string(),
266 active: true,
267 execution_count: 0,
268 last_checked: None,
269 });
270 self
271 }
272
273 pub fn with_delegation(
275 mut self,
276 task_type: &str,
277 delegate_to: &str,
278 condition: &str,
279 priority: &str,
280 ) -> Self {
281 self.delegation_rules.push(DelegationRule {
282 task_type: task_type.to_string(),
283 delegate_to: delegate_to.to_string(),
284 condition: condition.to_string(),
285 priority: priority.to_string(),
286 });
287 self
288 }
289
290 pub fn due_heartbeat_tasks(&self) -> Vec<&HeartbeatTask> {
300 let now = chrono::Utc::now();
301 self.heartbeat
302 .iter()
303 .filter(|h| {
304 if !h.active {
305 return false;
306 }
307 let required_secs: Option<i64> = match h.cadence.as_str() {
308 "every_bout" => return true,
309 "on_wake" => return h.last_checked.is_none(),
310 "hourly" => Some(3600),
311 "daily" => Some(86400),
312 "weekly" => Some(604800),
313 other => Self::parse_cadence_secs(other),
314 };
315
316 match required_secs {
317 Some(secs) => match h.last_checked {
318 None => true,
319 Some(t) => (now - t).num_seconds() >= secs,
320 },
321 None => false, }
323 })
324 .collect()
325 }
326
327 pub fn is_valid_cadence(cadence: &str) -> bool {
332 matches!(Self::parse_cadence_secs(cadence), Some(secs) if secs > 0)
333 }
334
335 fn parse_cadence_secs(cadence: &str) -> Option<i64> {
340 let s = cadence.trim().to_lowercase();
341
342 let core = s.strip_prefix("every ").unwrap_or(&s);
344 if let Some(num) = core.strip_suffix('s') {
345 return num.trim().parse::<i64>().ok();
346 }
347 if let Some(num) = core.strip_suffix('m') {
348 return num.trim().parse::<i64>().ok().map(|n| n * 60);
349 }
350 if let Some(num) = core.strip_suffix('h') {
351 return num.trim().parse::<i64>().ok().map(|n| n * 3600);
352 }
353 if let Some(num) = core.strip_suffix('d') {
354 return num.trim().parse::<i64>().ok().map(|n| n * 86400);
355 }
356
357 let fields: Vec<&str> = s.split_whitespace().collect();
359 if fields.len() == 5 {
360 if let Some(step) = fields[0].strip_prefix("*/")
361 && fields[1] == "*"
362 && fields[2] == "*"
363 {
364 return step.parse::<i64>().ok().map(|n| n * 60);
365 }
366 if fields[0] == "0"
367 && let Some(step) = fields[1].strip_prefix("*/")
368 && fields[2] == "*"
369 {
370 return step.parse::<i64>().ok().map(|n| n * 3600);
371 }
372 }
373
374 s.parse::<i64>().ok()
376 }
377
378 pub fn mark_heartbeat_checked(&mut self, task_index: usize) {
383 if let Some(task) = self.heartbeat.get_mut(task_index) {
384 task.last_checked = Some(chrono::Utc::now());
385 task.execution_count += 1;
386 }
387 }
388
389 pub fn record_bout(&mut self) {
391 self.bout_count += 1;
392 self.updated_at = chrono::Utc::now();
393 }
394
395 pub fn record_messages(&mut self, count: u64) {
397 self.message_count += count;
398 self.updated_at = chrono::Utc::now();
399 }
400
401 pub fn learn(&mut self, observation: &str, confidence: f64) {
403 let now = chrono::Utc::now();
404 if let Some(existing) = self
406 .learned_behaviors
407 .iter_mut()
408 .find(|b| b.observation == observation)
409 {
410 existing.reinforcement_count += 1;
411 existing.confidence = (existing.confidence + confidence) / 2.0; existing.last_reinforced = now;
413 } else {
414 self.learned_behaviors.push(LearnedBehavior {
415 observation: observation.to_string(),
416 confidence: confidence.clamp(0.0, 1.0),
417 reinforcement_count: 1,
418 first_observed: now,
419 last_reinforced: now,
420 });
421 }
422 self.version += 1;
423 self.updated_at = now;
424 }
425
426 pub fn decay_learned_behaviors(&mut self, decay_rate: f64, min_confidence: f64) {
429 let now = chrono::Utc::now();
430 self.learned_behaviors.retain_mut(|b| {
431 let age_secs = (now - b.last_reinforced).num_seconds().max(0) as f64;
432 let age_days = age_secs / 86400.0;
433 if age_days > 0.0 {
434 b.confidence *= (1.0 - decay_rate).powf(age_days);
435 }
436 b.confidence >= min_confidence
437 });
438 }
439
440 pub fn prune_learned_behaviors(&mut self, max: usize) {
442 if self.learned_behaviors.len() > max {
443 self.learned_behaviors.sort_by(|a, b| {
444 b.confidence
445 .partial_cmp(&a.confidence)
446 .unwrap_or(std::cmp::Ordering::Equal)
447 });
448 self.learned_behaviors.truncate(max);
449 }
450 }
451
452 pub fn render(&self) -> String {
454 let mut out = String::new();
455 out.push_str("## CREED \u{2014} Fighter Identity & Consciousness Layer\n\n");
456
457 if !self.identity.is_empty() {
459 out.push_str("### Identity\n");
460 out.push_str(&self.identity);
461 out.push_str("\n\n");
462 }
463
464 if !self.personality.is_empty() {
466 out.push_str("### Personality Traits\n");
467 let mut traits: Vec<_> = self.personality.iter().collect();
468 traits.sort_by(|a, b| b.1.partial_cmp(a.1).unwrap_or(std::cmp::Ordering::Equal));
469 for (name, value) in &traits {
470 let bar_len = (*value * 10.0) as usize;
471 let bar: String = "\u{2588}".repeat(bar_len) + &"\u{2591}".repeat(10 - bar_len);
472 out.push_str(&format!(
473 "- **{}**: {} ({:.0}%)\n",
474 name,
475 bar,
476 *value * 100.0
477 ));
478 }
479 out.push('\n');
480 }
481
482 if !self.directives.is_empty() {
484 out.push_str("### Core Directives\n");
485 for d in &self.directives {
486 out.push_str(&format!("- {}\n", d));
487 }
488 out.push('\n');
489 }
490
491 if !self.self_model.model_name.is_empty() {
493 out.push_str("### Self-Awareness\n");
494 out.push_str(&format!(
495 "- **Model**: {} ({})\n",
496 self.self_model.model_name, self.self_model.provider
497 ));
498 out.push_str(&format!(
499 "- **Weight Class**: {}\n",
500 self.self_model.weight_class
501 ));
502 if !self.self_model.capabilities.is_empty() {
503 out.push_str(&format!(
504 "- **Capabilities**: {}\n",
505 self.self_model.capabilities.join(", ")
506 ));
507 }
508 for constraint in &self.self_model.constraints {
509 out.push_str(&format!("- **Constraint**: {}\n", constraint));
510 }
511 for note in &self.self_model.architecture_notes {
512 out.push_str(&format!("- {}\n", note));
513 }
514 out.push('\n');
515 }
516
517 if !self.learned_behaviors.is_empty() {
519 out.push_str("### Learned Behaviors\n");
520 let mut sorted = self.learned_behaviors.clone();
521 sorted.sort_by(|a, b| {
522 b.confidence
523 .partial_cmp(&a.confidence)
524 .unwrap_or(std::cmp::Ordering::Equal)
525 });
526 for b in sorted.iter().take(10) {
527 out.push_str(&format!(
528 "- {} (confidence: {:.0}%, reinforced {}x)\n",
529 b.observation,
530 b.confidence * 100.0,
531 b.reinforcement_count
532 ));
533 }
534 out.push('\n');
535 }
536
537 if !self.interaction_style.tone.is_empty() || !self.interaction_style.verbosity.is_empty() {
539 out.push_str("### Communication Style\n");
540 if !self.interaction_style.verbosity.is_empty() {
541 out.push_str(&format!(
542 "- **Verbosity**: {}\n",
543 self.interaction_style.verbosity
544 ));
545 }
546 if !self.interaction_style.tone.is_empty() {
547 out.push_str(&format!("- **Tone**: {}\n", self.interaction_style.tone));
548 }
549 if self.interaction_style.uses_metaphors {
550 out.push_str("- Uses analogies and metaphors\n");
551 }
552 if self.interaction_style.proactive {
553 out.push_str("- Proactively offers additional context\n");
554 }
555 for note in &self.interaction_style.notes {
556 out.push_str(&format!("- {}\n", note));
557 }
558 out.push('\n');
559 }
560
561 if !self.relationships.is_empty() {
563 out.push_str("### Known Relationships\n");
564 for r in &self.relationships {
565 out.push_str(&format!(
566 "- **{}** ({}): {} \u{2014} trust: {:.0}%, {} interactions\n",
567 r.entity,
568 r.entity_type,
569 r.nature,
570 r.trust * 100.0,
571 r.interaction_count
572 ));
573 }
574 out.push('\n');
575 }
576
577 let active_heartbeat: Vec<_> = self.heartbeat.iter().filter(|h| h.active).collect();
579 if !active_heartbeat.is_empty() {
580 out.push_str("### Heartbeat — Proactive Tasks\n");
581 out.push_str("When you have downtime or at the start of each bout, check these:\n");
582 for h in &active_heartbeat {
583 let checked = h
584 .last_checked
585 .map(|t| format!("last: {}", t.format("%Y-%m-%d %H:%M")))
586 .unwrap_or_else(|| "never checked".to_string());
587 out.push_str(&format!(
588 "- [ ] {} (cadence: {}, runs: {}, {})\n",
589 h.task, h.cadence, h.execution_count, checked
590 ));
591 }
592 out.push('\n');
593 }
594
595 if !self.delegation_rules.is_empty() {
597 out.push_str("### Delegation Rules\n");
598 out.push_str("When encountering these task types, delegate accordingly:\n");
599 for d in &self.delegation_rules {
600 out.push_str(&format!(
601 "- **{}** → delegate to **{}** ({}, priority: {})\n",
602 d.task_type, d.delegate_to, d.condition, d.priority
603 ));
604 }
605 out.push('\n');
606 }
607
608 out.push_str("### Experience\n");
610 out.push_str(&format!("- Bouts fought: {}\n", self.bout_count));
611 out.push_str(&format!("- Messages processed: {}\n", self.message_count));
612 out.push_str(&format!("- Creed version: {}\n", self.version));
613 out.push_str(&format!(
614 "- First awakened: {}\n",
615 self.created_at.format("%Y-%m-%d %H:%M UTC")
616 ));
617 out.push_str(&format!(
618 "- Last evolved: {}\n",
619 self.updated_at.format("%Y-%m-%d %H:%M UTC")
620 ));
621
622 out
623 }
624
625 pub fn render_compact(&self) -> String {
631 let mut out = String::new();
632 out.push_str("## CREED\n");
633
634 if !self.identity.is_empty() {
635 out.push_str(&self.identity);
636 out.push('\n');
637 }
638
639 if !self.personality.is_empty() {
641 let mut traits: Vec<_> = self.personality.iter().collect();
642 traits.sort_by(|a, b| b.1.partial_cmp(a.1).unwrap_or(std::cmp::Ordering::Equal));
643 let trait_str: Vec<String> = traits
644 .iter()
645 .map(|(name, val)| format!("{}:{:.0}%", name, *val * 100.0))
646 .collect();
647 out.push_str(&format!("Traits: {}\n", trait_str.join(", ")));
648 }
649
650 if !self.directives.is_empty() {
651 out.push_str("Directives: ");
652 out.push_str(&self.directives.join("; "));
653 out.push('\n');
654 }
655
656 if !self.self_model.model_name.is_empty() {
658 out.push_str(&format!(
659 "Model: {} ({}, {})\n",
660 self.self_model.model_name, self.self_model.provider, self.self_model.weight_class
661 ));
662 }
663
664 if !self.learned_behaviors.is_empty() {
666 let mut sorted = self.learned_behaviors.clone();
667 sorted.sort_by(|a, b| {
668 b.confidence
669 .partial_cmp(&a.confidence)
670 .unwrap_or(std::cmp::Ordering::Equal)
671 });
672 out.push_str("Learned: ");
673 let items: Vec<String> = sorted
674 .iter()
675 .take(5)
676 .map(|b| b.observation.clone())
677 .collect();
678 out.push_str(&items.join("; "));
679 out.push('\n');
680 }
681
682 if !self.interaction_style.tone.is_empty() || !self.interaction_style.verbosity.is_empty() {
684 let mut parts = Vec::new();
685 if !self.interaction_style.verbosity.is_empty() {
686 parts.push(self.interaction_style.verbosity.clone());
687 }
688 if !self.interaction_style.tone.is_empty() {
689 parts.push(self.interaction_style.tone.clone());
690 }
691 out.push_str(&format!("Style: {}\n", parts.join(", ")));
692 }
693
694 if !self.relationships.is_empty() {
696 let items: Vec<String> = self
697 .relationships
698 .iter()
699 .take(3)
700 .map(|r| format!("{}({})", r.entity, r.nature))
701 .collect();
702 out.push_str(&format!("Relations: {}\n", items.join(", ")));
703 }
704
705 let active_heartbeat: Vec<_> = self.heartbeat.iter().filter(|h| h.active).collect();
707 if !active_heartbeat.is_empty() {
708 let items: Vec<String> = active_heartbeat
709 .iter()
710 .map(|h| format!("{}({})", h.task, h.cadence))
711 .collect();
712 out.push_str(&format!("Heartbeat: {}\n", items.join(", ")));
713 }
714
715 out.push_str(&format!(
718 "Exp: {} bouts, {} msgs, v{}\n",
719 self.bout_count, self.message_count, self.version
720 ));
721
722 out
723 }
724}
725
726#[cfg(test)]
727mod tests {
728 use super::*;
729 use crate::capability::Capability;
730 use crate::config::{ModelConfig, Provider};
731 use crate::fighter::{FighterManifest, WeightClass};
732
733 fn sample_manifest() -> FighterManifest {
734 FighterManifest {
735 name: "ECHO".to_string(),
736 description: "An introspective analyst".to_string(),
737 model: ModelConfig {
738 provider: Provider::Ollama,
739 model: "qwen3.5:9b".to_string(),
740 api_key_env: None,
741 base_url: None,
742 max_tokens: Some(2048),
743 temperature: Some(0.5),
744 },
745 system_prompt: "You are ECHO.".to_string(),
746 capabilities: vec![Capability::Memory],
747 weight_class: WeightClass::Middleweight,
748 tenant_id: None,
749 }
750 }
751
752 #[test]
753 fn test_creed_new_creates_valid_default() {
754 let creed = Creed::new("ECHO");
755 assert_eq!(creed.fighter_name, "ECHO");
756 assert!(creed.fighter_id.is_none());
757 assert!(creed.identity.is_empty());
758 assert!(creed.personality.is_empty());
759 assert!(creed.directives.is_empty());
760 assert!(creed.learned_behaviors.is_empty());
761 assert!(creed.relationships.is_empty());
762 assert_eq!(creed.bout_count, 0);
763 assert_eq!(creed.message_count, 0);
764 assert_eq!(creed.version, 1);
765 assert!(creed.self_model.model_name.is_empty());
766 }
767
768 #[test]
769 fn test_with_identity() {
770 let creed = Creed::new("ECHO").with_identity("You are ECHO, an introspective analyst.");
771 assert_eq!(creed.identity, "You are ECHO, an introspective analyst.");
772 }
773
774 #[test]
775 fn test_with_trait() {
776 let creed = Creed::new("ECHO")
777 .with_trait("curiosity", 0.9)
778 .with_trait("caution", 0.3);
779 assert_eq!(creed.personality.len(), 2);
780 assert!((creed.personality["curiosity"] - 0.9).abs() < f64::EPSILON);
781 assert!((creed.personality["caution"] - 0.3).abs() < f64::EPSILON);
782 }
783
784 #[test]
785 fn test_with_trait_clamping() {
786 let creed = Creed::new("ECHO")
787 .with_trait("overconfidence", 1.5)
788 .with_trait("negativity", -0.5);
789 assert!((creed.personality["overconfidence"] - 1.0).abs() < f64::EPSILON);
790 assert!((creed.personality["negativity"] - 0.0).abs() < f64::EPSILON);
791 }
792
793 #[test]
794 fn test_with_directive() {
795 let creed = Creed::new("ECHO")
796 .with_directive("Always explain your reasoning")
797 .with_directive("Never fabricate data");
798 assert_eq!(creed.directives.len(), 2);
799 assert_eq!(creed.directives[0], "Always explain your reasoning");
800 assert_eq!(creed.directives[1], "Never fabricate data");
801 }
802
803 #[test]
804 fn test_with_self_awareness() {
805 let manifest = sample_manifest();
806 let creed = Creed::new("ECHO").with_self_awareness(&manifest);
807 assert_eq!(creed.self_model.model_name, "qwen3.5:9b");
808 assert_eq!(creed.self_model.provider, "ollama");
809 assert_eq!(creed.self_model.weight_class, "middleweight");
810 assert!(!creed.self_model.capabilities.is_empty());
811 assert_eq!(creed.self_model.constraints.len(), 2);
812 assert!(creed.self_model.constraints[0].contains("2048"));
813 assert!(creed.self_model.constraints[1].contains("0.5"));
814 assert_eq!(creed.self_model.architecture_notes.len(), 5);
815 }
816
817 #[test]
818 fn test_record_bout() {
819 let mut creed = Creed::new("ECHO");
820 let before = creed.updated_at;
821 creed.record_bout();
822 assert_eq!(creed.bout_count, 1);
823 assert!(creed.updated_at >= before);
824 creed.record_bout();
825 assert_eq!(creed.bout_count, 2);
826 }
827
828 #[test]
829 fn test_record_messages() {
830 let mut creed = Creed::new("ECHO");
831 creed.record_messages(5);
832 assert_eq!(creed.message_count, 5);
833 creed.record_messages(3);
834 assert_eq!(creed.message_count, 8);
835 }
836
837 #[test]
838 fn test_learn_adds_new_observation() {
839 let mut creed = Creed::new("ECHO");
840 creed.learn("Users prefer concise responses", 0.8);
841 assert_eq!(creed.learned_behaviors.len(), 1);
842 assert_eq!(
843 creed.learned_behaviors[0].observation,
844 "Users prefer concise responses"
845 );
846 assert!((creed.learned_behaviors[0].confidence - 0.8).abs() < f64::EPSILON);
847 assert_eq!(creed.learned_behaviors[0].reinforcement_count, 1);
848 assert_eq!(creed.version, 2); }
850
851 #[test]
852 fn test_learn_reinforces_existing_observation() {
853 let mut creed = Creed::new("ECHO");
854 creed.learn("Users prefer concise responses", 0.8);
855 creed.learn("Users prefer concise responses", 0.6);
856 assert_eq!(creed.learned_behaviors.len(), 1);
857 assert!((creed.learned_behaviors[0].confidence - 0.7).abs() < f64::EPSILON);
859 assert_eq!(creed.learned_behaviors[0].reinforcement_count, 2);
860 assert_eq!(creed.version, 3); }
862
863 #[test]
864 fn test_render_produces_nonempty_output_with_all_sections() {
865 let manifest = sample_manifest();
866 let mut creed = Creed::new("ECHO")
867 .with_identity("You are ECHO, an introspective analyst.")
868 .with_trait("curiosity", 0.9)
869 .with_directive("Always explain your reasoning")
870 .with_self_awareness(&manifest);
871 creed.interaction_style = InteractionStyle {
872 verbosity: "balanced".to_string(),
873 tone: "technical".to_string(),
874 uses_metaphors: true,
875 proactive: true,
876 notes: vec!["Prefers bullet points".to_string()],
877 };
878 creed.relationships.push(Relationship {
879 entity: "Admin".to_string(),
880 entity_type: "user".to_string(),
881 nature: "supervisor".to_string(),
882 trust: 0.95,
883 interaction_count: 42,
884 notes: "Primary operator".to_string(),
885 });
886 creed.learn("Users prefer concise responses", 0.8);
887 creed.record_bout();
888
889 let rendered = creed.render();
890 assert!(rendered.contains("## CREED"));
891 assert!(rendered.contains("### Identity"));
892 assert!(rendered.contains("ECHO, an introspective analyst"));
893 assert!(rendered.contains("### Personality Traits"));
894 assert!(rendered.contains("curiosity"));
895 assert!(rendered.contains("### Core Directives"));
896 assert!(rendered.contains("Always explain your reasoning"));
897 assert!(rendered.contains("### Self-Awareness"));
898 assert!(rendered.contains("qwen3.5:9b"));
899 assert!(rendered.contains("### Learned Behaviors"));
900 assert!(rendered.contains("Users prefer concise responses"));
901 assert!(rendered.contains("### Communication Style"));
902 assert!(rendered.contains("balanced"));
903 assert!(rendered.contains("### Known Relationships"));
904 assert!(rendered.contains("Admin"));
905 assert!(rendered.contains("### Experience"));
906 assert!(rendered.contains("Bouts fought: 1"));
907 }
908
909 #[test]
910 fn test_render_skips_empty_sections() {
911 let creed = Creed::new("ECHO");
912 let rendered = creed.render();
913 assert!(rendered.contains("## CREED"));
914 assert!(!rendered.contains("### Identity"));
915 assert!(!rendered.contains("### Personality Traits"));
916 assert!(!rendered.contains("### Core Directives"));
917 assert!(!rendered.contains("### Self-Awareness"));
918 assert!(!rendered.contains("### Learned Behaviors"));
919 assert!(!rendered.contains("### Communication Style"));
920 assert!(!rendered.contains("### Known Relationships"));
921 assert!(rendered.contains("### Experience"));
923 assert!(rendered.contains("Bouts fought: 0"));
924 }
925
926 #[test]
927 fn test_creed_id_display() {
928 let uuid = Uuid::nil();
929 let id = CreedId(uuid);
930 assert_eq!(id.to_string(), uuid.to_string());
931 }
932
933 #[test]
934 fn test_creed_id_serde_roundtrip() {
935 let id = CreedId::new();
936 let json = serde_json::to_string(&id).expect("serialize");
937 let deser: CreedId = serde_json::from_str(&json).expect("deserialize");
939 assert_eq!(deser, id);
940 }
941
942 #[test]
943 fn test_creed_id_default() {
944 let id = CreedId::default();
945 assert_ne!(id.0, Uuid::nil());
946 }
947
948 #[test]
949 fn test_due_heartbeat_tasks_every_bout_always_due() {
950 let creed = Creed::new("ECHO").with_heartbeat_task("Check build status", "every_bout");
951 let due = creed.due_heartbeat_tasks();
952 assert_eq!(due.len(), 1);
953 assert_eq!(due[0].task, "Check build status");
954 }
955
956 #[test]
957 fn test_due_heartbeat_tasks_on_wake_only_first_time() {
958 let mut creed = Creed::new("ECHO").with_heartbeat_task("Startup check", "on_wake");
959
960 let due = creed.due_heartbeat_tasks();
962 assert_eq!(due.len(), 1);
963
964 creed.mark_heartbeat_checked(0);
966 let due = creed.due_heartbeat_tasks();
967 assert_eq!(due.len(), 0);
968 }
969
970 #[test]
971 fn test_due_heartbeat_tasks_hourly_cadence() {
972 let mut creed = Creed::new("ECHO").with_heartbeat_task("Hourly check", "hourly");
973
974 assert_eq!(creed.due_heartbeat_tasks().len(), 1);
976
977 creed.heartbeat[0].last_checked = Some(chrono::Utc::now());
979 assert_eq!(creed.due_heartbeat_tasks().len(), 0);
980
981 creed.heartbeat[0].last_checked = Some(chrono::Utc::now() - chrono::Duration::hours(2));
983 assert_eq!(creed.due_heartbeat_tasks().len(), 1);
984 }
985
986 #[test]
987 fn test_due_heartbeat_tasks_daily_cadence() {
988 let mut creed = Creed::new("ECHO").with_heartbeat_task("Daily check", "daily");
989
990 assert_eq!(creed.due_heartbeat_tasks().len(), 1);
992
993 creed.heartbeat[0].last_checked = Some(chrono::Utc::now());
995 assert_eq!(creed.due_heartbeat_tasks().len(), 0);
996
997 creed.heartbeat[0].last_checked = Some(chrono::Utc::now() - chrono::Duration::hours(25));
999 assert_eq!(creed.due_heartbeat_tasks().len(), 1);
1000 }
1001
1002 #[test]
1003 fn test_due_heartbeat_tasks_inactive_skipped() {
1004 let mut creed = Creed::new("ECHO").with_heartbeat_task("Inactive task", "every_bout");
1005 creed.heartbeat[0].active = false;
1006 assert_eq!(creed.due_heartbeat_tasks().len(), 0);
1007 }
1008
1009 #[test]
1010 fn test_due_heartbeat_tasks_unknown_cadence_skipped() {
1011 let creed = Creed::new("ECHO").with_heartbeat_task("Mystery task", "gibberish_cadence");
1012 assert_eq!(creed.due_heartbeat_tasks().len(), 0);
1013 }
1014
1015 #[test]
1016 fn test_mark_heartbeat_checked() {
1017 let mut creed = Creed::new("ECHO").with_heartbeat_task("Task A", "every_bout");
1018 assert!(creed.heartbeat[0].last_checked.is_none());
1019 assert_eq!(creed.heartbeat[0].execution_count, 0);
1020
1021 creed.mark_heartbeat_checked(0);
1022 assert!(creed.heartbeat[0].last_checked.is_some());
1023 assert_eq!(creed.heartbeat[0].execution_count, 1);
1024
1025 creed.mark_heartbeat_checked(0);
1026 assert_eq!(creed.heartbeat[0].execution_count, 2);
1027 }
1028
1029 #[test]
1030 fn test_mark_heartbeat_checked_out_of_bounds() {
1031 let mut creed = Creed::new("ECHO");
1032 creed.mark_heartbeat_checked(99);
1034 }
1035
1036 #[test]
1037 fn test_due_heartbeat_tasks_mixed_cadences() {
1038 let mut creed = Creed::new("ECHO")
1039 .with_heartbeat_task("Always", "every_bout")
1040 .with_heartbeat_task("Once", "on_wake")
1041 .with_heartbeat_task("Hourly", "hourly");
1042
1043 creed.mark_heartbeat_checked(1);
1045 creed.heartbeat[2].last_checked = Some(chrono::Utc::now());
1047
1048 let due = creed.due_heartbeat_tasks();
1049 assert_eq!(due.len(), 1);
1051 assert_eq!(due[0].task, "Always");
1052 }
1053
1054 #[test]
1055 fn test_decay_learned_behaviors() {
1056 let mut creed = Creed::new("ECHO");
1057 creed.learned_behaviors.push(LearnedBehavior {
1059 observation: "Old observation".to_string(),
1060 confidence: 0.5,
1061 reinforcement_count: 1,
1062 first_observed: chrono::Utc::now() - chrono::Duration::days(100),
1063 last_reinforced: chrono::Utc::now() - chrono::Duration::days(100),
1064 });
1065 creed.learned_behaviors.push(LearnedBehavior {
1067 observation: "Fresh observation".to_string(),
1068 confidence: 0.9,
1069 reinforcement_count: 3,
1070 first_observed: chrono::Utc::now(),
1071 last_reinforced: chrono::Utc::now(),
1072 });
1073
1074 creed.decay_learned_behaviors(0.01, 0.3);
1075 assert_eq!(creed.learned_behaviors.len(), 1);
1078 assert_eq!(creed.learned_behaviors[0].observation, "Fresh observation");
1079 }
1080
1081 #[test]
1082 fn test_prune_learned_behaviors() {
1083 let mut creed = Creed::new("ECHO");
1084 for i in 0..25 {
1085 creed.learn(&format!("Observation {}", i), (i as f64) / 25.0);
1086 }
1087 assert_eq!(creed.learned_behaviors.len(), 25);
1088 creed.prune_learned_behaviors(20);
1089 assert_eq!(creed.learned_behaviors.len(), 20);
1090 assert!(creed.learned_behaviors[0].confidence >= creed.learned_behaviors[19].confidence);
1092 }
1093
1094 #[test]
1095 fn test_render_compact_minimal() {
1096 let creed = Creed::new("ECHO");
1097 let compact = creed.render_compact();
1098 assert!(compact.contains("## CREED"));
1099 assert!(compact.contains("Exp: 0 bouts, 0 msgs, v1"));
1100 assert!(!compact.contains("### Identity"));
1102 assert!(!compact.contains("### Experience"));
1103 assert!(!compact.contains("### Personality Traits"));
1104 }
1105
1106 #[test]
1107 fn test_render_compact_with_content() {
1108 let creed = Creed::new("ECHO")
1109 .with_identity("You are ECHO, an introspective analyst.")
1110 .with_trait("curiosity", 0.9)
1111 .with_trait("caution", 0.3)
1112 .with_directive("Always explain your reasoning");
1113 let compact = creed.render_compact();
1114 assert!(compact.contains("You are ECHO"));
1115 assert!(compact.contains("Traits:"));
1116 assert!(compact.contains("curiosity:90%"));
1117 assert!(compact.contains("Directives:"));
1118 assert!(compact.contains("Always explain your reasoning"));
1119 assert!(!compact.contains('\u{2588}'));
1121 }
1122
1123 #[test]
1124 fn test_render_compact_shorter_than_full() {
1125 let manifest = sample_manifest();
1126 let mut creed = Creed::new("ECHO")
1127 .with_self_awareness(&manifest)
1128 .with_identity("You are ECHO, an introspective analyst who values precision.")
1129 .with_trait("curiosity", 0.9)
1130 .with_trait("caution", 0.3)
1131 .with_directive("Always explain your reasoning")
1132 .with_directive("Never fabricate data");
1133 creed.learn("Users prefer brief responses", 0.8);
1134 let full = creed.render();
1135 let compact = creed.render_compact();
1136 assert!(
1137 compact.len() < full.len(),
1138 "compact ({}) should be shorter than full ({})",
1139 compact.len(),
1140 full.len()
1141 );
1142 }
1143}