pub mod agent_loop;
pub mod automation_tools;
pub mod chat;
pub mod executor;
pub mod media_tools;
pub mod memory;
pub mod net_tools;
pub mod policy;
pub mod prompt;
pub mod register;
pub mod studio_tools;
pub mod substrate;
pub mod vision_tools;
use std::path::PathBuf;
use std::sync::Arc;
use async_trait::async_trait;
use car_engine::{Runtime, ToolEntry, ToolExecutor, ToolSchema};
use car_eventlog::EventLog;
use car_inference::InferenceEngine;
use car_policy::permission::PermissionTier;
use serde_json::Value;
use memory::MemoryTools;
pub use agent_loop::{
run_assistant_goal_loop, run_assistant_loop, run_assistant_loop_cancellable, ApprovalDecision,
ApprovalGate, AssistantConfig, AssistantEvent, AssistantOutcome, GoalLoopResult,
};
pub use chat::AssistantService;
pub use executor::GeneralExecutor;
pub use net_tools::NetTools;
pub use substrate::{bind_default_substrate, BoundEnvironment};
pub struct AssistantRuntime {
pub runtime: Runtime,
pub tools: Vec<Value>,
pub description: String,
pub sandboxed: bool,
pub gated_tools: Vec<String>,
pub fallback_notice: Option<String>,
}
struct ChainedDelegate(Vec<Arc<dyn ToolExecutor>>);
#[async_trait]
impl ToolExecutor for ChainedDelegate {
async fn execute(&self, tool: &str, params: &Value) -> Result<Value, String> {
for ex in &self.0 {
match ex.execute(tool, params).await {
Err(e) if e.starts_with("unknown tool") => continue,
other => return other,
}
}
Err(format!("unknown tool: '{tool}'"))
}
}
fn schema_from_def(def: &Value) -> ToolSchema {
ToolSchema {
name: def["name"].as_str().unwrap_or_default().to_string(),
description: def["description"].as_str().unwrap_or_default().to_string(),
parameters: def["parameters"].clone(),
returns: None,
idempotent: false,
cache_ttl_secs: None,
rate_limit: None,
}
}
fn tier_gated_tool_names(tools: &[Value], standing: PermissionTier) -> Vec<String> {
tools
.iter()
.filter_map(|def| {
let name = def.get("name").and_then(|v| v.as_str())?;
let tier = PermissionTier::from_str_opt(def.get("tier").and_then(|v| v.as_str())?)?;
(tier > standing).then(|| name.to_string())
})
.collect()
}
fn default_memory_path() -> PathBuf {
let home = std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."));
home.join(".car").join("memory").join("assistant.json")
}
pub async fn build_assistant_runtime(
engine: Arc<InferenceEngine>,
env: BoundEnvironment,
eventlog: Option<PathBuf>,
) -> AssistantRuntime {
let mut gated_tools: Vec<String> = if matches!(env.tier, PermissionTier::ReadOnly) {
["write_file", "edit_file", "shell"]
.iter()
.map(|s| s.to_string())
.collect()
} else {
Vec::new()
};
let clamp = !env.sandboxed;
let description = env.description.clone();
let sandboxed = env.sandboxed;
let fallback_notice = env.fallback_notice.clone();
let net: Arc<dyn ToolExecutor> = Arc::new(NetTools::new());
let mem: Arc<dyn ToolExecutor> = Arc::new(MemoryTools::open(default_memory_path()));
let media = Arc::new(media_tools::MediaTools::new(
engine.clone(),
env.root.clone(),
));
let studio = Arc::new(studio_tools::StudioMediaTools::new(env.root.clone()));
let vision = Arc::new(vision_tools::VisionTools::new(env.root.clone()));
let automation = Arc::new(automation_tools::AutomationTools::new());
let mut delegate_defs = net_tools::net_tool_defs();
delegate_defs.extend(MemoryTools::tool_defs());
delegate_defs.extend(media.tool_defs());
delegate_defs.extend(studio.tool_defs());
delegate_defs.extend(vision.tool_defs());
delegate_defs.extend(automation.tool_defs());
let media: Arc<dyn ToolExecutor> = media;
let studio: Arc<dyn ToolExecutor> = studio;
let vision: Arc<dyn ToolExecutor> = vision;
let automation: Arc<dyn ToolExecutor> = automation;
let delegate: Arc<dyn ToolExecutor> = Arc::new(ChainedDelegate(vec![
net, mem, media, studio, vision, automation,
]));
let executor = GeneralExecutor::new(env.substrate.clone(), env.root.clone(), clamp)
.with_delegate(delegate, delegate_defs);
let tools = executor.all_tool_defs();
gated_tools.extend(tier_gated_tool_names(&tools, env.tier));
let executor: Arc<dyn ToolExecutor> = Arc::new(executor);
let mut runtime = Runtime::new()
.with_inference(engine)
.with_executor(executor)
.with_substrate(env.substrate.clone());
if let Some(path) = eventlog {
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
runtime = runtime.with_event_log(EventLog::with_journal(path));
}
runtime.register_agent_basics().await;
let builtin_names: std::collections::HashSet<String> = car_engine::agent_basic_entries()
.into_iter()
.map(|e| e.schema.name)
.collect();
for def in &tools {
let name = def["name"].as_str().unwrap_or_default();
if name.is_empty() || builtin_names.contains(name) {
continue;
}
runtime
.register_tool_entry(ToolEntry::new(schema_from_def(def)).with_side_effects(true))
.await;
}
AssistantRuntime {
runtime,
tools,
description,
sandboxed,
gated_tools,
fallback_notice,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn tier_gating_derives_from_self_declared_tier() {
let tools = vec![
json!({"name": "read_file"}), json!({"name": "generate_image", "tier": "sandbox_edit"}),
json!({"name": "run_applescript", "tier": "full_access"}),
];
let g = tier_gated_tool_names(&tools, PermissionTier::ReadOnly);
assert!(g.contains(&"generate_image".to_string()));
assert!(g.contains(&"run_applescript".to_string()));
assert!(!g.contains(&"read_file".to_string()));
let g = tier_gated_tool_names(&tools, PermissionTier::SandboxEdit);
assert_eq!(g, vec!["run_applescript".to_string()]);
assert!(tier_gated_tool_names(&tools, PermissionTier::FullAccess).is_empty());
}
}