pub mod agent_loop;
pub mod chat;
pub mod executor;
pub mod memory;
pub mod net_tools;
pub mod policy;
pub mod prompt;
pub mod register;
pub mod substrate;
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_loop, run_assistant_loop_cancellable, ApprovalDecision, ApprovalGate,
AssistantConfig, AssistantEvent, AssistantOutcome,
};
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 default_memory_path() -> PathBuf {
let home = std::env::var("HOME")
.ok()
.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 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 mut delegate_defs = net_tools::net_tool_defs();
delegate_defs.extend(MemoryTools::tool_defs());
let delegate: Arc<dyn ToolExecutor> = Arc::new(ChainedDelegate(vec![net, mem]));
let executor = GeneralExecutor::new(env.substrate.clone(), env.root.clone(), clamp)
.with_delegate(delegate, delegate_defs);
let tools = executor.all_tool_defs();
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,
}
}