Files
tkmind_go/crates/goose/src/execution/manager.rs
T
Josh Soref f636a97faa Spelling (#7137)
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2026-02-11 14:35:24 +00:00

352 lines
12 KiB
Rust

use crate::agents::{Agent, AgentConfig};
use crate::config::paths::Paths;
use crate::config::permission::PermissionManager;
use crate::config::{Config, GooseMode};
use crate::scheduler::Scheduler;
use crate::scheduler_trait::SchedulerTrait;
use crate::session::SessionManager;
use anyhow::Result;
use lru::LruCache;
use std::num::NonZeroUsize;
use std::sync::Arc;
use tokio::sync::{OnceCell, RwLock};
use tracing::{debug, info};
const DEFAULT_MAX_SESSION: usize = 100;
static AGENT_MANAGER: OnceCell<Arc<AgentManager>> = OnceCell::const_new();
pub struct AgentManager {
sessions: Arc<RwLock<LruCache<String, Arc<Agent>>>>,
scheduler: Arc<dyn SchedulerTrait>,
session_manager: Arc<SessionManager>,
default_provider: Arc<RwLock<Option<Arc<dyn crate::providers::base::Provider>>>>,
}
impl AgentManager {
pub async fn new(
session_manager: Arc<SessionManager>,
schedule_file_path: std::path::PathBuf,
max_sessions: Option<usize>,
) -> Result<Self> {
let scheduler = Scheduler::new(schedule_file_path, session_manager.clone()).await?;
let capacity = NonZeroUsize::new(max_sessions.unwrap_or(DEFAULT_MAX_SESSION))
.unwrap_or_else(|| NonZeroUsize::new(100).unwrap());
let manager = Self {
sessions: Arc::new(RwLock::new(LruCache::new(capacity))),
scheduler,
session_manager,
default_provider: Arc::new(RwLock::new(None)),
};
Ok(manager)
}
pub async fn instance() -> Result<Arc<Self>> {
AGENT_MANAGER
.get_or_try_init(|| async {
let max_sessions = Config::global()
.get_goose_max_active_agents()
.unwrap_or(DEFAULT_MAX_SESSION);
let schedule_file_path = Paths::data_dir().join("schedule.json");
let session_manager = Arc::new(SessionManager::instance());
let manager =
Self::new(session_manager, schedule_file_path, Some(max_sessions)).await?;
Ok(Arc::new(manager))
})
.await
.cloned()
}
pub fn scheduler(&self) -> Arc<dyn SchedulerTrait> {
Arc::clone(&self.scheduler)
}
/// Get the shared SessionManager for session-only operations
pub fn session_manager(&self) -> &SessionManager {
&self.session_manager
}
pub async fn set_default_provider(&self, provider: Arc<dyn crate::providers::base::Provider>) {
debug!("Setting default provider on AgentManager");
*self.default_provider.write().await = Some(provider);
}
pub async fn get_or_create_agent(&self, session_id: String) -> Result<Arc<Agent>> {
{
let mut sessions = self.sessions.write().await;
if let Some(existing) = sessions.get(&session_id) {
return Ok(Arc::clone(existing));
}
}
let mode = Config::global().get_goose_mode().unwrap_or(GooseMode::Auto);
let permission_manager = PermissionManager::instance();
let config = AgentConfig::new(
Arc::clone(&self.session_manager),
permission_manager,
Some(Arc::clone(&self.scheduler)),
mode,
Config::global()
.get_goose_disable_session_naming()
.unwrap_or(false),
);
let agent = Arc::new(Agent::with_config(config));
if let Some(provider) = &*self.default_provider.read().await {
agent
.update_provider(Arc::clone(provider), &session_id)
.await?;
}
let mut sessions = self.sessions.write().await;
if let Some(existing) = sessions.get(&session_id) {
Ok(Arc::clone(existing))
} else {
sessions.put(session_id, agent.clone());
Ok(agent)
}
}
pub async fn remove_session(&self, session_id: &str) -> Result<()> {
let mut sessions = self.sessions.write().await;
sessions
.pop(session_id)
.ok_or_else(|| anyhow::anyhow!("Session {} not found", session_id))?;
info!("Removed session {}", session_id);
Ok(())
}
pub async fn has_session(&self, session_id: &str) -> bool {
self.sessions.read().await.contains(session_id)
}
pub async fn session_count(&self) -> usize {
self.sessions.read().await.len()
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use tempfile::TempDir;
use crate::execution::SessionExecutionMode;
use crate::session::SessionManager;
use super::AgentManager;
async fn create_test_manager(temp_dir: &TempDir) -> AgentManager {
let session_manager = Arc::new(SessionManager::new(temp_dir.path().to_path_buf()));
let schedule_path = temp_dir.path().join("schedule.json");
AgentManager::new(session_manager, schedule_path, Some(100))
.await
.unwrap()
}
#[test]
fn test_execution_mode_constructors() {
assert_eq!(
SessionExecutionMode::chat(),
SessionExecutionMode::Interactive
);
assert_eq!(
SessionExecutionMode::scheduled(),
SessionExecutionMode::Background
);
let parent = "parent-123".to_string();
assert_eq!(
SessionExecutionMode::task(parent.clone()),
SessionExecutionMode::SubTask {
parent_session: parent
}
);
}
#[tokio::test]
async fn test_session_isolation() {
let temp_dir = TempDir::new().unwrap();
let manager = create_test_manager(&temp_dir).await;
let session1 = uuid::Uuid::new_v4().to_string();
let session2 = uuid::Uuid::new_v4().to_string();
let agent1 = manager.get_or_create_agent(session1.clone()).await.unwrap();
let agent2 = manager.get_or_create_agent(session2.clone()).await.unwrap();
// Different sessions should have different agents
assert!(!Arc::ptr_eq(&agent1, &agent2));
// Getting the same session should return the same agent
let agent1_again = manager.get_or_create_agent(session1).await.unwrap();
assert!(Arc::ptr_eq(&agent1, &agent1_again));
}
#[tokio::test]
async fn test_session_limit() {
let temp_dir = TempDir::new().unwrap();
let manager = create_test_manager(&temp_dir).await;
let sessions: Vec<_> = (0..100).map(|i| format!("session-{}", i)).collect();
for session in &sessions {
manager.get_or_create_agent(session.clone()).await.unwrap();
}
// Create a new session after cleanup
let new_session = "new-session".to_string();
let _new_agent = manager.get_or_create_agent(new_session).await.unwrap();
assert_eq!(manager.session_count().await, 100);
}
#[tokio::test]
async fn test_remove_session() {
let temp_dir = TempDir::new().unwrap();
let manager = create_test_manager(&temp_dir).await;
let session = String::from("remove-test");
manager.get_or_create_agent(session.clone()).await.unwrap();
assert!(manager.has_session(&session).await);
manager.remove_session(&session).await.unwrap();
assert!(!manager.has_session(&session).await);
assert!(manager.remove_session(&session).await.is_err());
}
#[tokio::test]
async fn test_concurrent_access() {
let temp_dir = TempDir::new().unwrap();
let manager = Arc::new(create_test_manager(&temp_dir).await);
let session = String::from("concurrent-test");
let mut handles = vec![];
for _ in 0..10 {
let mgr = Arc::clone(&manager);
let sess = session.clone();
handles.push(tokio::spawn(async move {
mgr.get_or_create_agent(sess).await.unwrap()
}));
}
let agents: Vec<_> = futures::future::join_all(handles)
.await
.into_iter()
.map(|r| r.unwrap())
.collect();
for agent in &agents[1..] {
assert!(Arc::ptr_eq(&agents[0], agent));
}
assert_eq!(manager.session_count().await, 1);
}
#[tokio::test]
async fn test_concurrent_session_creation_race_condition() {
// Test that concurrent attempts to create the same new session ID
// result in only one agent being created (tests double-check pattern)
let temp_dir = TempDir::new().unwrap();
let manager = Arc::new(create_test_manager(&temp_dir).await);
let session_id = String::from("race-condition-test");
// Spawn multiple tasks trying to create the same NEW session simultaneously
let mut handles = vec![];
for _ in 0..20 {
let sess = session_id.clone();
let mgr_clone = Arc::clone(&manager);
handles.push(tokio::spawn(async move {
mgr_clone.get_or_create_agent(sess).await.unwrap()
}));
}
// Collect all agents
let agents: Vec<_> = futures::future::join_all(handles)
.await
.into_iter()
.map(|r| r.unwrap())
.collect();
for agent in &agents[1..] {
assert!(
Arc::ptr_eq(&agents[0], agent),
"All concurrent requests should get the same agent"
);
}
assert_eq!(manager.session_count().await, 1);
}
#[tokio::test]
async fn test_set_default_provider() {
use crate::providers::testprovider::TestProvider;
let temp_dir = TempDir::new().unwrap();
let manager = create_test_manager(&temp_dir).await;
// Create a test provider for replaying (doesn't need inner provider)
let temp_file = temp_dir.path().join("test_provider.json");
// Create an empty test provider (will fail on actual use but that's ok for this test)
std::fs::write(&temp_file, "{}").unwrap();
let test_provider = TestProvider::new_replaying(temp_file.to_str().unwrap()).unwrap();
manager.set_default_provider(Arc::new(test_provider)).await;
let session = String::from("provider-test");
let _agent = manager.get_or_create_agent(session.clone()).await.unwrap();
assert!(manager.has_session(&session).await);
}
#[tokio::test]
async fn test_eviction_updates_last_used() {
// Test that accessing a session updates its last_used timestamp
// and affects eviction order
let temp_dir = TempDir::new().unwrap();
let manager = create_test_manager(&temp_dir).await;
let sessions: Vec<_> = (0..100).map(|i| format!("session-{}", i)).collect();
for session in &sessions {
manager.get_or_create_agent(session.clone()).await.unwrap();
// Small delay to ensure different timestamps
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
}
// Access the first session again to update its last_used
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
manager
.get_or_create_agent(sessions[0].clone())
.await
.unwrap();
// Now create a 101st session - should evict session2 (least recently used)
let session101 = String::from("session-101");
manager
.get_or_create_agent(session101.clone())
.await
.unwrap();
assert!(manager.has_session(&sessions[0]).await);
assert!(!manager.has_session(&sessions[1]).await);
assert!(manager.has_session(&session101).await);
}
#[tokio::test]
async fn test_remove_nonexistent_session_error() {
// Test that removing a nonexistent session returns an error
let temp_dir = TempDir::new().unwrap();
let manager = create_test_manager(&temp_dir).await;
let session = String::from("never-created");
let result = manager.remove_session(&session).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not found"));
}
}