pe-core 0.1.0

Core types for Potential Expectations — messages, channels, state, traits
Documentation
//! Self-model — the agent's model of itself, users, and collective.
//!
//! Three context lenses that the cognitive graph draws from:
//! - **Self:** What am I? What can I do? What are my limits?
//! - **User:** Who am I talking to? What do they need?
//! - **Collective:** What's the team doing? What's the shared goal?
//!
//! Also contains the Negative Knowledge Store (structured "don'ts")
//! and Error/Failure Registry (structured failure log).

use crate::message::Message;
use serde::{Deserialize, Serialize};

/// The agent's model of itself and its context.
///
/// Users populate these — the library provides the structure.
/// The cognitive graph reads from all three during processing.
///
/// # Example
///
/// ```
/// use pe_core::self_model::SelfModel;
/// use pe_core::Message;
///
/// let model = SelfModel::new()
///     .with_self_context(vec![Message::system("I am a code review agent.")]);
/// assert_eq!(model.self_context.len(), 1);
/// ```
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SelfModel {
    /// Self-awareness: what am I? capabilities, limits, strengths, weaknesses.
    #[serde(default)]
    pub self_context: Vec<Message>,

    /// User model: who am I talking to? expertise, preferences, history.
    #[serde(default)]
    pub user_context: Vec<Message>,

    /// Collective context: team goal, shared knowledge, other agents' findings.
    #[serde(default)]
    pub collective_context: Vec<Message>,
}

impl SelfModel {
    /// Create an empty self-model.
    pub fn new() -> Self {
        Self::default()
    }

    /// Set self-awareness context.
    #[must_use]
    pub fn with_self_context(mut self, messages: Vec<Message>) -> Self {
        self.self_context = messages;
        self
    }

    /// Set user model context.
    #[must_use]
    pub fn with_user_context(mut self, messages: Vec<Message>) -> Self {
        self.user_context = messages;
        self
    }

    /// Set collective context.
    #[must_use]
    pub fn with_collective_context(mut self, messages: Vec<Message>) -> Self {
        self.collective_context = messages;
        self
    }
}

/// A structured constraint — something the agent learned NOT to do.
///
/// Stored in the Negative Knowledge Store, queryable by category.
/// Unlike free-text notes, these are structured rows that lobes
/// can filter: "give me all Critical constraints for category=API".
///
/// # Example
///
/// ```
/// use pe_core::self_model::{NegativeKnowledge, Severity};
///
/// let constraint = NegativeKnowledge::new(
///     "api_calls",
///     "Never send more than 100 items per batch",
///     Severity::High,
/// );
/// assert_eq!(constraint.category, "api_calls");
/// ```
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct NegativeKnowledge {
    /// Classification for structured queries.
    pub category: String,

    /// The constraint itself.
    pub constraint: String,

    /// How important this constraint is.
    pub severity: Severity,

    /// Where this constraint came from (e.g., "user feedback", "execution failure").
    pub source: String,

    /// Additional context about when/why this applies.
    #[serde(default)]
    pub context: String,

    /// ISO 8601 timestamp when recorded.
    #[serde(default)]
    pub created_at: String,
}

impl NegativeKnowledge {
    /// Create a new constraint.
    pub fn new(
        category: impl Into<String>,
        constraint: impl Into<String>,
        severity: Severity,
    ) -> Self {
        Self {
            category: category.into(),
            constraint: constraint.into(),
            severity,
            source: String::new(),
            context: String::new(),
            created_at: String::new(),
        }
    }

    /// Set the source of this constraint.
    #[must_use]
    pub fn with_source(mut self, source: impl Into<String>) -> Self {
        self.source = source.into();
        self
    }
}

/// Severity level for negative knowledge constraints.
///
/// Ordering: `Critical < High < Medium < Low` (by derive order).
/// Use [`is_at_least`](Severity::is_at_least) for intuitive filtering:
///
/// ```
/// use pe_core::self_model::Severity;
/// assert!(Severity::Critical.is_at_least(&Severity::High));  // Critical is at least High
/// assert!(!Severity::Low.is_at_least(&Severity::High));       // Low is NOT at least High
/// ```
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum Severity {
    /// Must never violate — hard constraint.
    Critical,
    /// Should avoid — strong guidance.
    High,
    /// Prefer to avoid — soft guidance.
    Medium,
    /// Informational — nice to know.
    Low,
}

impl Severity {
    /// Whether this severity is at least as severe as `minimum`.
    ///
    /// ```
    /// use pe_core::self_model::Severity;
    /// assert!(Severity::Critical.is_at_least(&Severity::Medium));
    /// assert!(Severity::High.is_at_least(&Severity::High));
    /// assert!(!Severity::Medium.is_at_least(&Severity::High));
    /// ```
    pub fn is_at_least(&self, minimum: &Severity) -> bool {
        self <= minimum
    }
}

/// A structured record of a failed attempt.
///
/// Every failure logged with full context enables pattern recognition:
/// "3 of the last 5 database failures were timeouts."
///
/// # Example
///
/// ```
/// use pe_core::self_model::FailureRecord;
///
/// let record = FailureRecord::new("database_migration", "direct ALTER TABLE")
///     .with_error_kind("timeout")
///     .with_root_cause("Table too large for online DDL");
/// assert_eq!(record.task_type, "database_migration");
/// ```
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FailureRecord {
    /// What kind of task was being attempted.
    pub task_type: String,

    /// What approach was tried.
    pub approach_tried: String,

    /// Classification of the error.
    #[serde(default)]
    pub error_kind: String,

    /// Root cause analysis (may be empty if unknown).
    #[serde(default)]
    pub root_cause: Option<String>,

    /// How it was resolved (may be empty if unresolved).
    #[serde(default)]
    pub resolution: Option<String>,

    /// ISO 8601 timestamp when the failure occurred.
    #[serde(default)]
    pub timestamp: String,

    /// Execution scope where the failure happened.
    #[serde(default)]
    pub scope_id: String,
}

impl FailureRecord {
    /// Create a new failure record.
    pub fn new(task_type: impl Into<String>, approach_tried: impl Into<String>) -> Self {
        Self {
            task_type: task_type.into(),
            approach_tried: approach_tried.into(),
            error_kind: String::new(),
            root_cause: None,
            resolution: None,
            timestamp: String::new(),
            scope_id: String::new(),
        }
    }

    /// Set the error classification.
    #[must_use]
    pub fn with_error_kind(mut self, kind: impl Into<String>) -> Self {
        self.error_kind = kind.into();
        self
    }

    /// Set the root cause.
    #[must_use]
    pub fn with_root_cause(mut self, cause: impl Into<String>) -> Self {
        self.root_cause = Some(cause.into());
        self
    }

    /// Set the resolution.
    #[must_use]
    pub fn with_resolution(mut self, resolution: impl Into<String>) -> Self {
        self.resolution = Some(resolution.into());
        self
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_self_model_creation() {
        let model = SelfModel::new()
            .with_self_context(vec![Message::system("I analyze code.")])
            .with_user_context(vec![Message::system("User is a senior dev.")]);
        assert_eq!(model.self_context.len(), 1);
        assert_eq!(model.user_context.len(), 1);
        assert!(model.collective_context.is_empty());
    }

    #[test]
    fn test_negative_knowledge() {
        let nk = NegativeKnowledge::new("api", "max 100 items per batch", Severity::High)
            .with_source("production incident");
        assert_eq!(nk.category, "api");
        assert_eq!(nk.severity, Severity::High);
        assert_eq!(nk.source, "production incident");
    }

    #[test]
    fn test_severity_ordering() {
        assert!(Severity::Critical < Severity::High);
        assert!(Severity::High < Severity::Medium);
        assert!(Severity::Medium < Severity::Low);
    }

    #[test]
    fn test_failure_record() {
        let record = FailureRecord::new("db_migration", "ALTER TABLE")
            .with_error_kind("timeout")
            .with_root_cause("table too large")
            .with_resolution("use pt-online-schema-change");
        assert_eq!(record.task_type, "db_migration");
        assert_eq!(record.error_kind, "timeout");
        assert_eq!(record.root_cause.as_deref(), Some("table too large"));
        assert_eq!(
            record.resolution.as_deref(),
            Some("use pt-online-schema-change")
        );
    }

    #[test]
    fn test_negative_knowledge_serialization() {
        let nk = NegativeKnowledge::new("parsing", "don't use regex for HTML", Severity::Critical);
        let json = serde_json::to_string(&nk).unwrap();
        let back: NegativeKnowledge = serde_json::from_str(&json).unwrap();
        assert_eq!(back, nk);
    }

    #[test]
    fn test_failure_record_serialization() {
        let record = FailureRecord::new("auth", "JWT validation").with_error_kind("expired_token");
        let json = serde_json::to_string(&record).unwrap();
        let back: FailureRecord = serde_json::from_str(&json).unwrap();
        assert_eq!(back, record);
    }
}