pe-core 0.1.0

Core types for Potential Expectations — messages, channels, state, traits
Documentation
//! Agent — bounded execution context.
//!
//! Agent = Identity + Boundaries. As thin as an ID + prompt (minimum viable)
//! or as rich as a fully governed execution context (production grade).
//! Based on Decision 19 (REVISED) and Groups 30-31 of the pre-plan.

use crate::boundaries::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Unique agent identifier
pub type AgentId = String;

/// The canonical agent type — bounded execution context.
///
/// Identity is the floor, not the ceiling. All boundary fields have sensible
/// defaults (fully open). An agent with only id + system_prompt works like
/// a thin identity agent. Boundaries are opt-in, not opt-out.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Agent {
    // === Identity (the floor) ===
    pub id: AgentId,
    pub system_prompt: SystemPrompt,
    pub model_preference: ModelPreference,
    pub declared_tools: Vec<String>,
    pub routing_examples: Vec<String>,
    pub capabilities: AgentCapabilities,
    #[serde(default)]
    pub tool_access: AgentToolAccess,
    #[serde(default)]
    pub tool_class_policy: AgentToolClassPolicy,

    // === Boundaries (what makes it production-grade) ===
    pub permissions: PermissionSet,
    pub guardrails: Vec<Guardrail>,
    pub context_budget: ContextBudget,
    pub tool_policy: ToolPolicy,
    pub communication_rules: CommunicationRules,
    pub write_governance: WriteGovernance,

    // === Cognitive Architecture (optional inner subgraph) ===
    /// When present, the agent's reasoning is decomposed into parallel
    /// cognitive streams (emotional, logical, antithink, etc.) that run
    /// as an inner subgraph before the main LLM call.
    /// When `None`, normal single-call execution. Zero overhead.
    #[serde(default)]
    pub cognitive_architecture: Option<crate::cognitive::CognitiveArchitecture>,

    // === Extension ===
    #[serde(default)]
    pub metadata: HashMap<String, serde_json::Value>,
}

// ── Identity types ────────────────────────────────────────────────────

/// System prompt — either a static string or a template.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SystemPrompt {
    Static(String),
    Template {
        template: String,
        variables: HashMap<String, String>,
    },
}

/// Model preference with fallback chain.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelPreference {
    pub primary: String,
    #[serde(default)]
    pub fallbacks: Vec<String>,
    #[serde(default)]
    pub params: HashMap<String, serde_json::Value>,
}

/// What this agent declares it can do.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AgentCapabilities {
    #[serde(default)]
    pub can_read_files: bool,
    #[serde(default)]
    pub can_write_files: bool,
    #[serde(default)]
    pub can_execute_code: bool,
    #[serde(default)]
    pub can_search_web: bool,
    #[serde(default)]
    pub custom: HashMap<String, bool>,
}

/// First-class tool classification owned by the agent boundary surface.
///
/// These classes describe how tools should be reasoned about and governed,
/// not whether runtime policy will ultimately allow them. Resolution may make
/// them available to the runtime, but review-safe, approval, budgets, write
/// governance, and communication rules still apply afterwards.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AgentToolAccess {
    #[serde(default)]
    pub collective_context_tools: Vec<String>,
    #[serde(default)]
    pub scoped_context_tools: Vec<String>,
    #[serde(default)]
    pub maintenance_tools: Vec<String>,
    #[serde(default)]
    pub scoped_memory_write_tools: Vec<String>,
    #[serde(default)]
    pub collective_memory_write_tools: Vec<String>,
    #[serde(default)]
    pub action_tools: Vec<String>,
    #[serde(default)]
    pub coordination_query_tools: Vec<String>,
    #[serde(default)]
    pub coordination_transfer_tools: Vec<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AgentToolClass {
    CollectiveContext,
    ScopedContext,
    Maintenance,
    ScopedMemoryWrite,
    CollectiveMemoryWrite,
    Action,
    CoordinationQuery,
    CoordinationTransfer,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AgentToolClassPolicy {
    pub collective_context: ToolClassConstraint,
    pub scoped_context: ToolClassConstraint,
    pub maintenance: ToolClassConstraint,
    pub scoped_memory_write: ToolClassConstraint,
    pub collective_memory_write: ToolClassConstraint,
    pub action: ToolClassConstraint,
    pub coordination_query: ToolClassConstraint,
    pub coordination_transfer: ToolClassConstraint,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub enum ToolClassConstraint {
    #[default]
    Inherit,
    Allowed,
    RequiresApproval,
    Denied,
}

// ── Agent builder ─────────────────────────────────────────────────────

impl Agent {
    /// Create a minimal agent — just identity, all boundaries open.
    pub fn new(id: impl Into<String>, system_prompt: impl Into<String>) -> Self {
        Self {
            id: id.into(),
            system_prompt: SystemPrompt::Static(system_prompt.into()),
            model_preference: ModelPreference {
                primary: "default".into(),
                fallbacks: vec![],
                params: HashMap::new(),
            },
            declared_tools: vec![],
            routing_examples: vec![],
            capabilities: AgentCapabilities::default(),
            tool_access: AgentToolAccess::default(),
            tool_class_policy: AgentToolClassPolicy::default(),
            permissions: PermissionSet::default(),
            guardrails: vec![],
            context_budget: ContextBudget::default(),
            tool_policy: ToolPolicy::default(),
            communication_rules: CommunicationRules::default(),
            write_governance: WriteGovernance::default(),
            cognitive_architecture: None,
            metadata: HashMap::new(),
        }
    }

    /// Enable cognitive architecture on this agent.
    ///
    /// When set, the agent's inner world runs before each LLM call,
    /// enriching context through parallel lobe processing and synthesis.
    /// When `None` (default), zero overhead — standard agent behavior.
    #[must_use]
    pub fn with_cognitive_architecture(
        mut self,
        arch: crate::cognitive::CognitiveArchitecture,
    ) -> Self {
        self.cognitive_architecture = Some(arch);
        self
    }

    #[must_use]
    pub fn with_tool_access(mut self, tool_access: AgentToolAccess) -> Self {
        self.tool_access = tool_access;
        self
    }

    #[must_use]
    pub fn with_tool_class_policy(mut self, tool_class_policy: AgentToolClassPolicy) -> Self {
        self.tool_class_policy = tool_class_policy;
        self
    }
}

impl AgentToolAccess {
    #[must_use]
    pub fn tool_names(&self) -> Vec<String> {
        let mut names = std::collections::BTreeSet::new();
        names.extend(self.collective_context_tools.iter().cloned());
        names.extend(self.scoped_context_tools.iter().cloned());
        names.extend(self.maintenance_tools.iter().cloned());
        names.extend(self.scoped_memory_write_tools.iter().cloned());
        names.extend(self.collective_memory_write_tools.iter().cloned());
        names.extend(self.action_tools.iter().cloned());
        names.extend(self.coordination_query_tools.iter().cloned());
        names.extend(self.coordination_transfer_tools.iter().cloned());
        names.into_iter().collect()
    }

    #[must_use]
    pub fn with_collective_context_tools(mut self, tools: Vec<String>) -> Self {
        self.collective_context_tools = tools;
        self
    }

    #[must_use]
    pub fn with_scoped_context_tools(mut self, tools: Vec<String>) -> Self {
        self.scoped_context_tools = tools;
        self
    }

    #[must_use]
    pub fn with_maintenance_tools(mut self, tools: Vec<String>) -> Self {
        self.maintenance_tools = tools;
        self
    }

    #[must_use]
    pub fn with_scoped_memory_write_tools(mut self, tools: Vec<String>) -> Self {
        self.scoped_memory_write_tools = tools;
        self
    }

    #[must_use]
    pub fn with_collective_memory_write_tools(mut self, tools: Vec<String>) -> Self {
        self.collective_memory_write_tools = tools;
        self
    }

    #[must_use]
    pub fn with_action_tools(mut self, tools: Vec<String>) -> Self {
        self.action_tools = tools;
        self
    }

    #[must_use]
    pub fn with_coordination_query_tools(mut self, tools: Vec<String>) -> Self {
        self.coordination_query_tools = tools;
        self
    }

    #[must_use]
    pub fn with_coordination_transfer_tools(mut self, tools: Vec<String>) -> Self {
        self.coordination_transfer_tools = tools;
        self
    }

    #[must_use]
    pub fn classify_tool(&self, tool_name: &str) -> Option<AgentToolClass> {
        if self
            .collective_context_tools
            .iter()
            .any(|name| name == tool_name)
        {
            Some(AgentToolClass::CollectiveContext)
        } else if self
            .scoped_context_tools
            .iter()
            .any(|name| name == tool_name)
        {
            Some(AgentToolClass::ScopedContext)
        } else if self.maintenance_tools.iter().any(|name| name == tool_name) {
            Some(AgentToolClass::Maintenance)
        } else if self
            .scoped_memory_write_tools
            .iter()
            .any(|name| name == tool_name)
        {
            Some(AgentToolClass::ScopedMemoryWrite)
        } else if self
            .collective_memory_write_tools
            .iter()
            .any(|name| name == tool_name)
        {
            Some(AgentToolClass::CollectiveMemoryWrite)
        } else if self.action_tools.iter().any(|name| name == tool_name) {
            Some(AgentToolClass::Action)
        } else if self
            .coordination_query_tools
            .iter()
            .any(|name| name == tool_name)
        {
            Some(AgentToolClass::CoordinationQuery)
        } else if self
            .coordination_transfer_tools
            .iter()
            .any(|name| name == tool_name)
        {
            Some(AgentToolClass::CoordinationTransfer)
        } else {
            None
        }
    }
}

impl AgentToolClassPolicy {
    #[must_use]
    pub fn constraint_for(&self, class: AgentToolClass) -> &ToolClassConstraint {
        match class {
            AgentToolClass::CollectiveContext => &self.collective_context,
            AgentToolClass::ScopedContext => &self.scoped_context,
            AgentToolClass::Maintenance => &self.maintenance,
            AgentToolClass::ScopedMemoryWrite => &self.scoped_memory_write,
            AgentToolClass::CollectiveMemoryWrite => &self.collective_memory_write,
            AgentToolClass::Action => &self.action,
            AgentToolClass::CoordinationQuery => &self.coordination_query,
            AgentToolClass::CoordinationTransfer => &self.coordination_transfer,
        }
    }

    #[must_use]
    pub fn with_collective_context(mut self, constraint: ToolClassConstraint) -> Self {
        self.collective_context = constraint;
        self
    }

    #[must_use]
    pub fn with_scoped_context(mut self, constraint: ToolClassConstraint) -> Self {
        self.scoped_context = constraint;
        self
    }

    #[must_use]
    pub fn with_maintenance(mut self, constraint: ToolClassConstraint) -> Self {
        self.maintenance = constraint;
        self
    }

    #[must_use]
    pub fn with_scoped_memory_write(mut self, constraint: ToolClassConstraint) -> Self {
        self.scoped_memory_write = constraint;
        self
    }

    #[must_use]
    pub fn with_collective_memory_write(mut self, constraint: ToolClassConstraint) -> Self {
        self.collective_memory_write = constraint;
        self
    }

    #[must_use]
    pub fn with_action(mut self, constraint: ToolClassConstraint) -> Self {
        self.action = constraint;
        self
    }

    #[must_use]
    pub fn with_coordination_query(mut self, constraint: ToolClassConstraint) -> Self {
        self.coordination_query = constraint;
        self
    }

    #[must_use]
    pub fn with_coordination_transfer(mut self, constraint: ToolClassConstraint) -> Self {
        self.coordination_transfer = constraint;
        self
    }
}

impl AgentToolClass {
    #[must_use]
    pub fn stable_name(self) -> &'static str {
        match self {
            AgentToolClass::CollectiveContext => "collective_context",
            AgentToolClass::ScopedContext => "scoped_context",
            AgentToolClass::Maintenance => "maintenance",
            AgentToolClass::ScopedMemoryWrite => "scoped_memory_write",
            AgentToolClass::CollectiveMemoryWrite => "collective_memory_write",
            AgentToolClass::Action => "action",
            AgentToolClass::CoordinationQuery => "coordination_query",
            AgentToolClass::CoordinationTransfer => "coordination_transfer",
        }
    }
}