byob 0.3.1

byob (bring your own brain): a native desktop AI coworker for any OpenAI-compatible endpoint
//! The in-tree parts of byob's bundled plugins (file, artifact, chart and code-running tools,
//! registered once; the prompt sections they name) and the plugins themselves; the user's
//! own subagents (`<config>/agents/*.md`); and where the engine finds plugins (the config
//! folder's `plugins`, `settings.json` `plugin_dirs`, `--plugin`). The engine loads plugins
//! itself, per chat and workspace, and reloads them when their files change.

use std::path::PathBuf;
use std::sync::Arc;

use byob_agent::PluginsConfig;
use byob_exec::{Exec, Mode};
use byob_plugins::AgentDef;
use byob_tools::Registry;

/// What [`load`] found besides tools.
pub struct Loaded {
    /// The user's own subagents (`<config>/agents`); plugins' come per chat.
    pub agents: Vec<AgentDef>,
    /// Workspace environment settings for the engine (with run_command / run_python).
    pub envs: Option<Arc<dyn byob_tools::EnvControl>>,
    /// Where the engine finds plugins.
    pub plugins: PluginsConfig,
}

/// Register the built-in tools into `tools` (the artifact tools for the engine's
/// `artifact_types`); `linked`: `--plugin` folders.
pub fn load(tools: &mut Registry, artifact_types: &byob_tools::ArtifactTypes, linked: Vec<PathBuf>) -> Loaded {
    byob_tools::register_builtins_with(tools, artifact_types);

    let paths = byob_store::Paths::resolve();
    // run_command / run_python (+ install_packages with Nix or pixi, else
    // install_python_packages with uv; install_system_packages where brew/winget is the
    // last resort; containers with Docker/Podman). The sandbox helper is this binary
    // (`byob __sandbox-exec`, see main).
    let mode = Mode::from_env();
    let mut exec_prompt = None;
    let mut envs: Option<Arc<dyn byob_tools::EnvControl>> = None;
    let settings = paths.as_ref().map(|p| byob_store::Settings::load(&p.settings()).value).unwrap_or_default();
    match &mode {
        Ok(mode) => {
            let data = paths.as_ref().ok().map(|p| p.data_dir.clone());
            let containers = settings.containers.clone();
            let exec = Exec::with_containers(mode.clone(), data, containers.image, containers.runtime.as_deref());
            exec_prompt = exec.prompt();
            envs = Some(exec.envs.clone());
            byob_exec::register(tools, exec);
        }
        Err(e) => log::warn!("no run_command/run_python: {e}"),
    }
    // Plugin scripts and local MCP servers go through the same sandbox (unless turned off).
    let sandbox = match mode {
        Ok(Mode::Sandboxed(sb)) => Some(sb),
        _ => None,
    };

    // The built-in features are bundled plugins (inside the binary) naming these tools,
    // artifact types and prompt sections by id; the code-running section is ours to add.
    let natives = byob_agent::Natives::builtin().prompt("exec", move |_| exec_prompt.clone().unwrap_or_default());
    // The bundled `sheets` plugin's backend: spreadsheets in-process.
    #[cfg(feature = "native-sheets")]
    let natives = natives.backend(byob_sheets::ID, byob_sheets::factory());
    let mut plugins = PluginsConfig {
        linked,
        python: byob_plugins::default_python(),
        sandbox,
        bundled: byob_agent::natives::bundled_plugins(),
        natives,
        ..Default::default()
    };
    match &paths {
        Ok(paths) => {
            plugins.global_roots.push(paths.config_dir.join("plugins"));
            plugins.global_roots.extend(settings.plugin_dirs.iter().cloned());
            plugins.global_skills = Some(paths.config_dir.join("skills"));
            plugins.scratch_root = Some(paths.data_dir.join("chats"));
            plugins.python_root = Some(paths.data_dir.join("plugin-python"));
            plugins.bundled_cache = Some(paths.data_dir.join("bundled-plugins"));
        }
        Err(e) => log::warn!("no config folder ({e}); only --plugin folders are loaded"),
    }

    let agents = match &paths {
        Ok(paths) => {
            let (agents, errors) = byob_plugins::load_agents_dir(&paths.config_dir.join("agents"));
            for e in errors {
                log::warn!("agent {e}");
            }
            agents
        }
        Err(_) => Vec::new(),
    };
    if !agents.is_empty() {
        log::info!("{} subagent(s) of your own: {}", agents.len(), agents.iter().map(|a| a.name.as_str()).collect::<Vec<_>>().join(", "));
    }
    Loaded { agents, envs, plugins }
}