Skip to main content

pe_core/
self_model.rs

1//! Self-model — the agent's model of itself, users, and collective.
2//!
3//! Three context lenses that the cognitive graph draws from:
4//! - **Self:** What am I? What can I do? What are my limits?
5//! - **User:** Who am I talking to? What do they need?
6//! - **Collective:** What's the team doing? What's the shared goal?
7//!
8//! Also contains the Negative Knowledge Store (structured "don'ts")
9//! and Error/Failure Registry (structured failure log).
10
11use crate::message::Message;
12use serde::{Deserialize, Serialize};
13
14/// The agent's model of itself and its context.
15///
16/// Users populate these — the library provides the structure.
17/// The cognitive graph reads from all three during processing.
18///
19/// # Example
20///
21/// ```
22/// use pe_core::self_model::SelfModel;
23/// use pe_core::Message;
24///
25/// let model = SelfModel::new()
26///     .with_self_context(vec![Message::system("I am a code review agent.")]);
27/// assert_eq!(model.self_context.len(), 1);
28/// ```
29#[derive(Debug, Clone, Default, Serialize, Deserialize)]
30pub struct SelfModel {
31    /// Self-awareness: what am I? capabilities, limits, strengths, weaknesses.
32    #[serde(default)]
33    pub self_context: Vec<Message>,
34
35    /// User model: who am I talking to? expertise, preferences, history.
36    #[serde(default)]
37    pub user_context: Vec<Message>,
38
39    /// Collective context: team goal, shared knowledge, other agents' findings.
40    #[serde(default)]
41    pub collective_context: Vec<Message>,
42}
43
44impl SelfModel {
45    /// Create an empty self-model.
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    /// Set self-awareness context.
51    #[must_use]
52    pub fn with_self_context(mut self, messages: Vec<Message>) -> Self {
53        self.self_context = messages;
54        self
55    }
56
57    /// Set user model context.
58    #[must_use]
59    pub fn with_user_context(mut self, messages: Vec<Message>) -> Self {
60        self.user_context = messages;
61        self
62    }
63
64    /// Set collective context.
65    #[must_use]
66    pub fn with_collective_context(mut self, messages: Vec<Message>) -> Self {
67        self.collective_context = messages;
68        self
69    }
70}
71
72/// A structured constraint — something the agent learned NOT to do.
73///
74/// Stored in the Negative Knowledge Store, queryable by category.
75/// Unlike free-text notes, these are structured rows that lobes
76/// can filter: "give me all Critical constraints for category=API".
77///
78/// # Example
79///
80/// ```
81/// use pe_core::self_model::{NegativeKnowledge, Severity};
82///
83/// let constraint = NegativeKnowledge::new(
84///     "api_calls",
85///     "Never send more than 100 items per batch",
86///     Severity::High,
87/// );
88/// assert_eq!(constraint.category, "api_calls");
89/// ```
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
91pub struct NegativeKnowledge {
92    /// Classification for structured queries.
93    pub category: String,
94
95    /// The constraint itself.
96    pub constraint: String,
97
98    /// How important this constraint is.
99    pub severity: Severity,
100
101    /// Where this constraint came from (e.g., "user feedback", "execution failure").
102    pub source: String,
103
104    /// Additional context about when/why this applies.
105    #[serde(default)]
106    pub context: String,
107
108    /// ISO 8601 timestamp when recorded.
109    #[serde(default)]
110    pub created_at: String,
111}
112
113impl NegativeKnowledge {
114    /// Create a new constraint.
115    pub fn new(
116        category: impl Into<String>,
117        constraint: impl Into<String>,
118        severity: Severity,
119    ) -> Self {
120        Self {
121            category: category.into(),
122            constraint: constraint.into(),
123            severity,
124            source: String::new(),
125            context: String::new(),
126            created_at: String::new(),
127        }
128    }
129
130    /// Set the source of this constraint.
131    #[must_use]
132    pub fn with_source(mut self, source: impl Into<String>) -> Self {
133        self.source = source.into();
134        self
135    }
136}
137
138/// Severity level for negative knowledge constraints.
139///
140/// Ordering: `Critical < High < Medium < Low` (by derive order).
141/// Use [`is_at_least`](Severity::is_at_least) for intuitive filtering:
142///
143/// ```
144/// use pe_core::self_model::Severity;
145/// assert!(Severity::Critical.is_at_least(&Severity::High));  // Critical is at least High
146/// assert!(!Severity::Low.is_at_least(&Severity::High));       // Low is NOT at least High
147/// ```
148#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
149#[non_exhaustive]
150pub enum Severity {
151    /// Must never violate — hard constraint.
152    Critical,
153    /// Should avoid — strong guidance.
154    High,
155    /// Prefer to avoid — soft guidance.
156    Medium,
157    /// Informational — nice to know.
158    Low,
159}
160
161impl Severity {
162    /// Whether this severity is at least as severe as `minimum`.
163    ///
164    /// ```
165    /// use pe_core::self_model::Severity;
166    /// assert!(Severity::Critical.is_at_least(&Severity::Medium));
167    /// assert!(Severity::High.is_at_least(&Severity::High));
168    /// assert!(!Severity::Medium.is_at_least(&Severity::High));
169    /// ```
170    pub fn is_at_least(&self, minimum: &Severity) -> bool {
171        self <= minimum
172    }
173}
174
175/// A structured record of a failed attempt.
176///
177/// Every failure logged with full context enables pattern recognition:
178/// "3 of the last 5 database failures were timeouts."
179///
180/// # Example
181///
182/// ```
183/// use pe_core::self_model::FailureRecord;
184///
185/// let record = FailureRecord::new("database_migration", "direct ALTER TABLE")
186///     .with_error_kind("timeout")
187///     .with_root_cause("Table too large for online DDL");
188/// assert_eq!(record.task_type, "database_migration");
189/// ```
190#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
191pub struct FailureRecord {
192    /// What kind of task was being attempted.
193    pub task_type: String,
194
195    /// What approach was tried.
196    pub approach_tried: String,
197
198    /// Classification of the error.
199    #[serde(default)]
200    pub error_kind: String,
201
202    /// Root cause analysis (may be empty if unknown).
203    #[serde(default)]
204    pub root_cause: Option<String>,
205
206    /// How it was resolved (may be empty if unresolved).
207    #[serde(default)]
208    pub resolution: Option<String>,
209
210    /// ISO 8601 timestamp when the failure occurred.
211    #[serde(default)]
212    pub timestamp: String,
213
214    /// Execution scope where the failure happened.
215    #[serde(default)]
216    pub scope_id: String,
217}
218
219impl FailureRecord {
220    /// Create a new failure record.
221    pub fn new(task_type: impl Into<String>, approach_tried: impl Into<String>) -> Self {
222        Self {
223            task_type: task_type.into(),
224            approach_tried: approach_tried.into(),
225            error_kind: String::new(),
226            root_cause: None,
227            resolution: None,
228            timestamp: String::new(),
229            scope_id: String::new(),
230        }
231    }
232
233    /// Set the error classification.
234    #[must_use]
235    pub fn with_error_kind(mut self, kind: impl Into<String>) -> Self {
236        self.error_kind = kind.into();
237        self
238    }
239
240    /// Set the root cause.
241    #[must_use]
242    pub fn with_root_cause(mut self, cause: impl Into<String>) -> Self {
243        self.root_cause = Some(cause.into());
244        self
245    }
246
247    /// Set the resolution.
248    #[must_use]
249    pub fn with_resolution(mut self, resolution: impl Into<String>) -> Self {
250        self.resolution = Some(resolution.into());
251        self
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    #[test]
260    fn test_self_model_creation() {
261        let model = SelfModel::new()
262            .with_self_context(vec![Message::system("I analyze code.")])
263            .with_user_context(vec![Message::system("User is a senior dev.")]);
264        assert_eq!(model.self_context.len(), 1);
265        assert_eq!(model.user_context.len(), 1);
266        assert!(model.collective_context.is_empty());
267    }
268
269    #[test]
270    fn test_negative_knowledge() {
271        let nk = NegativeKnowledge::new("api", "max 100 items per batch", Severity::High)
272            .with_source("production incident");
273        assert_eq!(nk.category, "api");
274        assert_eq!(nk.severity, Severity::High);
275        assert_eq!(nk.source, "production incident");
276    }
277
278    #[test]
279    fn test_severity_ordering() {
280        assert!(Severity::Critical < Severity::High);
281        assert!(Severity::High < Severity::Medium);
282        assert!(Severity::Medium < Severity::Low);
283    }
284
285    #[test]
286    fn test_failure_record() {
287        let record = FailureRecord::new("db_migration", "ALTER TABLE")
288            .with_error_kind("timeout")
289            .with_root_cause("table too large")
290            .with_resolution("use pt-online-schema-change");
291        assert_eq!(record.task_type, "db_migration");
292        assert_eq!(record.error_kind, "timeout");
293        assert_eq!(record.root_cause.as_deref(), Some("table too large"));
294        assert_eq!(
295            record.resolution.as_deref(),
296            Some("use pt-online-schema-change")
297        );
298    }
299
300    #[test]
301    fn test_negative_knowledge_serialization() {
302        let nk = NegativeKnowledge::new("parsing", "don't use regex for HTML", Severity::Critical);
303        let json = serde_json::to_string(&nk).unwrap();
304        let back: NegativeKnowledge = serde_json::from_str(&json).unwrap();
305        assert_eq!(back, nk);
306    }
307
308    #[test]
309    fn test_failure_record_serialization() {
310        let record = FailureRecord::new("auth", "JWT validation").with_error_kind("expired_token");
311        let json = serde_json::to_string(&record).unwrap();
312        let back: FailureRecord = serde_json::from_str(&json).unwrap();
313        assert_eq!(back, record);
314    }
315}