Skip to main content

rustant_core/multi/
isolation.rs

1//! Agent isolation — each agent gets its own memory and safety context.
2//!
3//! `AgentContext` bundles a unique ID, name, memory system, safety guardian,
4//! and optional parent reference, ensuring agents cannot interfere with
5//! each other's state.
6
7use crate::config::SafetyConfig;
8use crate::memory::MemorySystem;
9use crate::safety::SafetyGuardian;
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12use std::path::PathBuf;
13use uuid::Uuid;
14
15/// Resource limits for an agent.
16#[derive(Debug, Clone, Default, Serialize, Deserialize)]
17pub struct ResourceLimits {
18    pub max_memory_mb: Option<u64>,
19    pub max_tokens_per_turn: Option<u64>,
20    pub max_tool_calls: Option<u32>,
21    pub max_runtime_secs: Option<u64>,
22}
23
24/// Status of an agent.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26pub enum AgentStatus {
27    Idle,
28    Running,
29    Waiting,
30    Terminated,
31}
32
33/// Isolated execution context for a single agent.
34pub struct AgentContext {
35    /// Unique identifier for this agent.
36    pub agent_id: Uuid,
37    /// Human-readable name.
38    pub name: String,
39    /// Dedicated memory system (not shared with other agents).
40    pub memory: MemorySystem,
41    /// Dedicated safety guardian.
42    pub safety: SafetyGuardian,
43    /// Parent agent ID, if this agent was spawned by another.
44    pub parent_id: Option<Uuid>,
45    /// Per-agent sandbox working directory.
46    pub workspace_dir: Option<PathBuf>,
47    /// Per-agent LLM model override.
48    pub llm_override: Option<String>,
49    /// Per-agent resource constraints.
50    pub resource_limits: ResourceLimits,
51    /// When this agent was created.
52    pub created_at: DateTime<Utc>,
53    /// Current status.
54    pub status: AgentStatus,
55}
56
57impl AgentContext {
58    /// Create a new agent context with the given name.
59    pub fn new(name: impl Into<String>, window_size: usize, safety_config: SafetyConfig) -> Self {
60        Self {
61            agent_id: Uuid::new_v4(),
62            name: name.into(),
63            memory: MemorySystem::new(window_size),
64            safety: SafetyGuardian::new(safety_config),
65            parent_id: None,
66            workspace_dir: None,
67            llm_override: None,
68            resource_limits: ResourceLimits::default(),
69            created_at: Utc::now(),
70            status: AgentStatus::Idle,
71        }
72    }
73
74    /// Create a child context, linking back to a parent agent.
75    pub fn new_child(
76        name: impl Into<String>,
77        parent_id: Uuid,
78        window_size: usize,
79        safety_config: SafetyConfig,
80    ) -> Self {
81        Self {
82            agent_id: Uuid::new_v4(),
83            name: name.into(),
84            memory: MemorySystem::new(window_size),
85            safety: SafetyGuardian::new(safety_config),
86            parent_id: Some(parent_id),
87            workspace_dir: None,
88            llm_override: None,
89            resource_limits: ResourceLimits::default(),
90            created_at: Utc::now(),
91            status: AgentStatus::Idle,
92        }
93    }
94
95    /// Whether this context belongs to a child agent.
96    pub fn is_child(&self) -> bool {
97        self.parent_id.is_some()
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use crate::config::SafetyConfig;
105
106    #[test]
107    fn test_agent_context_new() {
108        let ctx = AgentContext::new("test-agent", 10, SafetyConfig::default());
109        assert_eq!(ctx.name, "test-agent");
110        assert!(!ctx.is_child());
111        assert!(ctx.parent_id.is_none());
112    }
113
114    #[test]
115    fn test_agent_context_child() {
116        let parent_id = Uuid::new_v4();
117        let ctx = AgentContext::new_child("child-agent", parent_id, 10, SafetyConfig::default());
118        assert!(ctx.is_child());
119        assert_eq!(ctx.parent_id, Some(parent_id));
120    }
121
122    #[test]
123    fn test_agent_context_isolation() {
124        // Two agents should have completely separate memory systems.
125        let mut ctx1 = AgentContext::new("agent-1", 10, SafetyConfig::default());
126        let mut ctx2 = AgentContext::new("agent-2", 10, SafetyConfig::default());
127
128        ctx1.memory.working.set_goal("Goal A");
129        ctx2.memory.working.set_goal("Goal B");
130
131        assert_eq!(ctx1.memory.working.current_goal.as_deref(), Some("Goal A"));
132        assert_eq!(ctx2.memory.working.current_goal.as_deref(), Some("Goal B"));
133        assert_ne!(ctx1.agent_id, ctx2.agent_id);
134    }
135
136    #[test]
137    fn test_agent_context_unique_ids() {
138        let a = AgentContext::new("a", 5, SafetyConfig::default());
139        let b = AgentContext::new("b", 5, SafetyConfig::default());
140        assert_ne!(a.agent_id, b.agent_id);
141    }
142
143    #[test]
144    fn test_agent_context_with_workspace() {
145        let mut ctx = AgentContext::new("test", 10, SafetyConfig::default());
146        assert!(ctx.workspace_dir.is_none());
147        ctx.workspace_dir = Some(PathBuf::from("/tmp/agent-workspace"));
148        assert_eq!(
149            ctx.workspace_dir.as_deref(),
150            Some(std::path::Path::new("/tmp/agent-workspace"))
151        );
152    }
153
154    #[test]
155    fn test_agent_context_with_llm_override() {
156        let mut ctx = AgentContext::new("test", 10, SafetyConfig::default());
157        assert!(ctx.llm_override.is_none());
158        ctx.llm_override = Some("claude-3-opus".into());
159        assert_eq!(ctx.llm_override.as_deref(), Some("claude-3-opus"));
160    }
161
162    #[test]
163    fn test_resource_limits_default_unbounded() {
164        let limits = ResourceLimits::default();
165        assert!(limits.max_memory_mb.is_none());
166        assert!(limits.max_tokens_per_turn.is_none());
167        assert!(limits.max_tool_calls.is_none());
168        assert!(limits.max_runtime_secs.is_none());
169    }
170
171    #[test]
172    fn test_resource_limits_custom() {
173        let limits = ResourceLimits {
174            max_memory_mb: Some(512),
175            max_tokens_per_turn: Some(4096),
176            max_tool_calls: Some(50),
177            max_runtime_secs: Some(300),
178        };
179        assert_eq!(limits.max_memory_mb, Some(512));
180        assert_eq!(limits.max_tool_calls, Some(50));
181    }
182
183    #[test]
184    fn test_agent_status_transitions() {
185        let mut ctx = AgentContext::new("test", 10, SafetyConfig::default());
186        assert_eq!(ctx.status, AgentStatus::Idle);
187        ctx.status = AgentStatus::Running;
188        assert_eq!(ctx.status, AgentStatus::Running);
189        ctx.status = AgentStatus::Waiting;
190        assert_eq!(ctx.status, AgentStatus::Waiting);
191        ctx.status = AgentStatus::Terminated;
192        assert_eq!(ctx.status, AgentStatus::Terminated);
193    }
194
195    #[test]
196    fn test_agent_context_created_at() {
197        let before = chrono::Utc::now();
198        let ctx = AgentContext::new("test", 10, SafetyConfig::default());
199        let after = chrono::Utc::now();
200        assert!(ctx.created_at >= before);
201        assert!(ctx.created_at <= after);
202    }
203
204    #[test]
205    fn test_new_child_inherits_defaults() {
206        let parent_id = Uuid::new_v4();
207        let ctx = AgentContext::new_child("child", parent_id, 10, SafetyConfig::default());
208        assert!(ctx.workspace_dir.is_none());
209        assert!(ctx.llm_override.is_none());
210        assert!(ctx.resource_limits.max_memory_mb.is_none());
211        assert_eq!(ctx.status, AgentStatus::Idle);
212    }
213
214    #[test]
215    fn test_resource_limits_none_means_unlimited() {
216        let limits = ResourceLimits {
217            max_memory_mb: None,
218            max_tokens_per_turn: None,
219            max_tool_calls: None,
220            max_runtime_secs: None,
221        };
222        // All None means no limits applied
223        assert!(limits.max_memory_mb.is_none());
224        assert!(limits.max_tokens_per_turn.is_none());
225        assert!(limits.max_tool_calls.is_none());
226        assert!(limits.max_runtime_secs.is_none());
227    }
228}