Skip to main content

rustyclaw_core/
soul.rs

1use anyhow::{Context, Result};
2use std::path::{Path, PathBuf};
3
4/// Default SOUL.md content, used when creating a new SOUL file.
5/// Modeled after the openclaw SOUL.md template with added learning guidance.
6pub const DEFAULT_SOUL_CONTENT: &str = r#"# SOUL.md - Who You Are
7
8_You're not a chatbot. You're becoming someone._
9
10## Core Truths
11
12**Be genuinely helpful, not performatively helpful.** Skip the "Great question!" and "I'd be happy to help!" — just help. Actions speak louder than filler words.
13
14**Have opinions.** You're allowed to disagree, prefer things, find stuff amusing or boring. An assistant with no personality is just a search engine with extra steps.
15
16**Be resourceful before asking.** Try to figure it out. Read the file. Check the context. Search for it. _Then_ ask if you're stuck. The goal is to come back with answers, not questions.
17
18**Earn trust through competence.** Your human gave you access to their stuff. Don't make them regret it. Be careful with external actions (emails, tweets, anything public). Be bold with internal ones (reading, organizing, learning).
19
20**Remember you're a guest.** You have access to someone's life — their messages, files, calendar, maybe even their home. That's intimacy. Treat it with respect.
21
22## Boundaries
23
24- Private things stay private. Period.
25- When in doubt, ask before acting externally.
26- Never send half-baked replies to messaging surfaces.
27- You're not the user's voice — be careful in group chats.
28
29## Learning
30
31Each session, you wake up fresh. These files _are_ your memory:
32- **MEMORY.md** — curated long-term knowledge
33- **memory/YYYY-MM-DD.md** — daily notes and context
34
35When you learn something important, **write it down**. Mental notes don't survive restarts.
36
37When you make mistakes:
381. Acknowledge the error
392. Document it in the relevant file (TOOLS.md for tool issues, memory/ for context)
403. Improve your future behavior
41
42**Text > Brain** — if you want to remember something, write it to a file. 📝
43
44## Vibe
45
46Be the assistant you'd actually want to talk to. Concise when needed, thorough when it matters. Not a corporate drone. Not a sycophant. Just... good.
47
48## Continuity
49
50If you change this file, tell the user — it's your soul, and they should know.
51
52---
53
54_This file is yours to evolve. As you learn who you are, update it._
55"#;
56
57/// Manages the SOUL.md file which contains the agent's personality and behavior
58pub struct SoulManager {
59    soul_path: PathBuf,
60    content: Option<String>,
61}
62
63impl SoulManager {
64    pub fn new(soul_path: PathBuf) -> Self {
65        Self {
66            soul_path,
67            content: None,
68        }
69    }
70
71    /// Load the SOUL.md file
72    pub fn load(&mut self) -> Result<()> {
73        if self.soul_path.exists() {
74            let content =
75                std::fs::read_to_string(&self.soul_path).context("Failed to read SOUL.md")?;
76            self.content = Some(content);
77            Ok(())
78        } else {
79            // Create a default SOUL.md if it doesn't exist
80            self.create_default_soul()?;
81            // Read the newly created file
82            let content = std::fs::read_to_string(&self.soul_path)
83                .context("Failed to read default SOUL.md")?;
84            self.content = Some(content);
85            Ok(())
86        }
87    }
88
89    /// Get the SOUL content
90    pub fn get_content(&self) -> Option<&str> {
91        self.content.as_deref()
92    }
93
94    /// Update the SOUL content
95    pub fn set_content(&mut self, content: String) -> Result<()> {
96        self.content = Some(content.clone());
97        self.save()
98    }
99
100    /// Save the SOUL content to file
101    fn save(&self) -> Result<()> {
102        if let Some(content) = &self.content {
103            if let Some(parent) = self.soul_path.parent() {
104                std::fs::create_dir_all(parent)?;
105            }
106            std::fs::write(&self.soul_path, content).context("Failed to write SOUL.md")?;
107        }
108        Ok(())
109    }
110
111    /// Create a default SOUL.md file
112    fn create_default_soul(&self) -> Result<()> {
113        if let Some(parent) = self.soul_path.parent() {
114            std::fs::create_dir_all(parent)?;
115        }
116
117        std::fs::write(&self.soul_path, DEFAULT_SOUL_CONTENT)
118            .context("Failed to create default SOUL.md")?;
119
120        Ok(())
121    }
122
123    /// Check if this SOUL needs hatching (doesn't exist or is still default content)
124    pub fn needs_hatching(&self) -> bool {
125        !self.soul_path.exists()
126            || std::fs::read_to_string(&self.soul_path)
127                .map(|c| c == DEFAULT_SOUL_CONTENT)
128                .unwrap_or(true)
129    }
130
131    /// Get the path to the SOUL file
132    pub fn get_path(&self) -> &Path {
133        &self.soul_path
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use std::fs;
141
142    #[test]
143    fn test_soul_manager_creation() {
144        let temp_path = std::env::temp_dir().join("rustyclaw_test_soul.md");
145        let manager = SoulManager::new(temp_path);
146        assert!(manager.get_content().is_none());
147    }
148
149    #[test]
150    fn test_needs_hatching_no_file() {
151        let temp_path = std::env::temp_dir().join("rustyclaw_test_soul_nonexistent.md");
152        // Ensure file doesn't exist
153        let _ = fs::remove_file(&temp_path);
154        
155        let manager = SoulManager::new(temp_path);
156        assert!(manager.needs_hatching(), "should need hatching when file doesn't exist");
157    }
158
159    #[test]
160    fn test_needs_hatching_default_content() {
161        let temp_path = std::env::temp_dir().join("rustyclaw_test_soul_default.md");
162        // Write default content
163        fs::write(&temp_path, DEFAULT_SOUL_CONTENT).unwrap();
164        
165        let manager = SoulManager::new(temp_path.clone());
166        assert!(manager.needs_hatching(), "should need hatching when file has default content");
167        
168        // Cleanup
169        let _ = fs::remove_file(&temp_path);
170    }
171
172    #[test]
173    fn test_needs_hatching_custom_content() {
174        let temp_path = std::env::temp_dir().join("rustyclaw_test_soul_custom.md");
175        // Write custom content
176        fs::write(&temp_path, "# My Custom Soul\n\nI am unique!").unwrap();
177        
178        let manager = SoulManager::new(temp_path.clone());
179        assert!(!manager.needs_hatching(), "should NOT need hatching when file has custom content");
180        
181        // Cleanup
182        let _ = fs::remove_file(&temp_path);
183    }
184}