use std::path::Path;
use std::sync::Arc;
use clawft_platform::Platform;
use clawft_types::config::AgentsConfig;
use super::context::ContextBuilder;
use super::memory::MemoryStore;
use super::skills::SkillsLoader;
use crate::session::SessionManager;
use crate::tools::registry::ToolRegistry;
pub struct AgentRuntime<P: Platform> {
agent_id: String,
sessions: Arc<SessionManager<P>>,
context: ContextBuilder<P>,
tools: Arc<ToolRegistry>,
config: AgentsConfig,
}
impl<P: Platform> AgentRuntime<P> {
pub fn for_agent(
agent_id: impl Into<String>,
workspace: &Path,
platform: Arc<P>,
memory: Arc<MemoryStore<P>>,
skills: Arc<SkillsLoader<P>>,
config: AgentsConfig,
) -> Self {
let agent_id = agent_id.into();
let sessions_dir = workspace.join("sessions").join(&agent_id);
let sessions = Arc::new(SessionManager::with_dir(platform.clone(), sessions_dir));
let context = ContextBuilder::new(config.clone(), memory, skills, platform);
let tools = Arc::new(ToolRegistry::new());
Self {
agent_id,
sessions,
context,
tools,
config,
}
}
pub fn with_components(
agent_id: impl Into<String>,
sessions: Arc<SessionManager<P>>,
context: ContextBuilder<P>,
tools: Arc<ToolRegistry>,
config: AgentsConfig,
) -> Self {
Self {
agent_id: agent_id.into(),
sessions,
context,
tools,
config,
}
}
pub fn agent_id(&self) -> &str {
&self.agent_id
}
pub fn sessions(&self) -> &Arc<SessionManager<P>> {
&self.sessions
}
pub fn context(&self) -> &ContextBuilder<P> {
&self.context
}
pub fn into_context(self) -> ContextBuilder<P> {
self.context
}
pub fn tools(&self) -> &Arc<ToolRegistry> {
&self.tools
}
pub fn tools_mut(&mut self) -> &mut ToolRegistry {
Arc::get_mut(&mut self.tools).expect("AgentRuntime::tools_mut: registry already shared")
}
pub fn config(&self) -> &AgentsConfig {
&self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
use clawft_platform::NativePlatform;
use clawft_types::config::{AgentDefaults, AgentsConfig};
fn test_config(agent_id: &str) -> AgentsConfig {
AgentsConfig {
defaults: AgentDefaults {
workspace: format!("/tmp/clawft-runtime-test/{agent_id}"),
model: "test-model".into(),
max_tokens: 4096,
temperature: 0.5,
max_tool_iterations: 10,
memory_window: 50,
},
..AgentsConfig::default()
}
}
#[tokio::test]
async fn separate_runtimes_have_isolated_sessions() {
let platform = Arc::new(NativePlatform::new());
let memory = Arc::new(MemoryStore::new(platform.clone()).unwrap());
let skills = Arc::new(SkillsLoader::new(platform.clone()).unwrap());
let pid = std::process::id();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let root = std::env::temp_dir().join(format!("clawft-runtime-iso-{pid}-{nanos}"));
let ws_a = root.join("agent-a");
let ws_b = root.join("agent-b");
std::fs::create_dir_all(ws_a.join("sessions").join("agent-a")).unwrap();
std::fs::create_dir_all(ws_b.join("sessions").join("agent-b")).unwrap();
let rt_a = AgentRuntime::for_agent(
"agent-a",
&ws_a,
platform.clone(),
memory.clone(),
skills.clone(),
test_config("agent-a"),
);
let rt_b = AgentRuntime::for_agent(
"agent-b",
&ws_b,
platform.clone(),
memory.clone(),
skills.clone(),
test_config("agent-b"),
);
assert_eq!(rt_a.agent_id(), "agent-a");
assert_eq!(rt_b.agent_id(), "agent-b");
assert!(!Arc::ptr_eq(rt_a.sessions(), rt_b.sessions()));
assert!(!Arc::ptr_eq(rt_a.tools(), rt_b.tools()));
let mut session_a = rt_a.sessions().get_or_create("test:chat-a").await.unwrap();
session_a.add_message("user", "hello from a", None);
rt_a.sessions().save_session(&session_a).await.unwrap();
let mut session_b = rt_b.sessions().get_or_create("test:chat-b").await.unwrap();
session_b.add_message("user", "hello from b", None);
rt_b.sessions().save_session(&session_b).await.unwrap();
let b_lookup = rt_b.sessions().get_or_create("test:chat-a").await.unwrap();
assert!(
b_lookup.messages.is_empty(),
"agent-b session should not contain agent-a's messages"
);
let _ = std::fs::remove_dir_all(&root);
}
#[tokio::test]
async fn with_components_assembles_runtime() {
let platform = Arc::new(NativePlatform::new());
let memory = Arc::new(MemoryStore::new(platform.clone()).unwrap());
let skills = Arc::new(SkillsLoader::new(platform.clone()).unwrap());
let pid = std::process::id();
let dir = std::env::temp_dir().join(format!("clawft-rt-wc-{pid}"));
std::fs::create_dir_all(&dir).unwrap();
let sessions = Arc::new(SessionManager::with_dir(platform.clone(), dir.clone()));
let cfg = test_config("custom");
let context = ContextBuilder::new(cfg.clone(), memory, skills, platform);
let tools = Arc::new(ToolRegistry::new());
let rt = AgentRuntime::with_components("custom", sessions, context, tools, cfg);
assert_eq!(rt.agent_id(), "custom");
assert_eq!(rt.config().defaults.model, "test-model");
let _ = std::fs::remove_dir_all(&dir);
}
#[tokio::test]
async fn tools_mut_allows_registration_before_share() {
let platform = Arc::new(NativePlatform::new());
let memory = Arc::new(MemoryStore::new(platform.clone()).unwrap());
let skills = Arc::new(SkillsLoader::new(platform.clone()).unwrap());
let pid = std::process::id();
let ws = std::env::temp_dir().join(format!("clawft-rt-tools-{pid}"));
std::fs::create_dir_all(ws.join("sessions").join("ag")).unwrap();
let mut rt =
AgentRuntime::for_agent("ag", &ws, platform, memory, skills, test_config("ag"));
assert_eq!(rt.tools().len(), 0);
use crate::tools::registry::Tool;
use async_trait::async_trait;
struct DummyTool;
#[async_trait]
impl Tool for DummyTool {
fn name(&self) -> &str {
"dummy"
}
fn description(&self) -> &str {
"test"
}
fn parameters(&self) -> serde_json::Value {
serde_json::json!({"type": "object"})
}
async fn execute(
&self,
_args: serde_json::Value,
) -> Result<serde_json::Value, crate::tools::registry::ToolError> {
Ok(serde_json::json!({}))
}
}
rt.tools_mut().register(Arc::new(DummyTool));
assert_eq!(rt.tools().len(), 1);
let _ = std::fs::remove_dir_all(&ws);
}
}