opencrabs 0.5.1

The autonomous, self-improving AI agent. Single Rust binary. Every channel. Recommended: the 40MB prebuilt binary for macOS, Linux and Windows: https://github.com/adolfousier/opencrabs/releases
//! Shared agent tool-registry construction.
//!
//! The interactive startup (TUI + channels) and the headless multi-profile
//! cron daemon both need an `AgentService` backed by the same core tools.
//! Keeping the registration in ONE place is what stops the daemon from running
//! cron jobs against an empty registry ("No tools registered" →
//! "Tool not found: bash") and keeps the two entry points from drifting.

use std::sync::Arc;

use crate::brain::tools::registry::ToolRegistry;
use crate::config::Config;
use crate::db::Database;

/// Register the dependency-light CORE agent tools: file ops, shell, search,
/// workflow, memory/brain, session/channel/cron/a2a/config/slash/rename,
/// follow-up, tool discovery, the config-dependent tools, sub-agent + team
/// orchestration, and RSI. These need only the DB pool + config (managers are
/// created inline and used nowhere else), so both entry points can share one
/// registry definition.
///
/// Browser, channel-send, media, and rebuild/evolve tools are registered
/// separately by the interactive path — they need managers the daemon lacks.
///
/// `headless` gates the two tools that are meaningless without a live user
/// surface (#129, owner-ruled design in fork issue body, 2026-09-07):
/// - `session_notify` (parks the notification until a turn-end that never
///   comes on a one-shot run; sub-agents report via the harness's
///   final-message relay instead)
/// - `suggest_options` (renders nowhere headless — no TUI, no channel
///   handler to consume the option message)
///
/// Excluding them here is the PRIMARY gate; the tool-level context guard
/// (`ToolExecutionContext.headless` → hard `ToolError`) is the
/// belt-and-braces backstop, so a registration leak fails loudly instead
/// of silently parking a verdict. Interactive callers pass `false`.
pub(crate) fn register_core_agent_tools(
    tool_registry: &Arc<ToolRegistry>,
    db: &Database,
    config: &Config,
    headless: bool,
) -> Arc<crate::brain::tools::subagent::SubAgentManager> {
    use crate::brain::tools::{
        bash::BashTool, code_exec::CodeExecTool, config_tool::ConfigTool, context::ContextTool,
        doc_gen::GenerateDocumentTool, doc_parser::DocParserTool, edit::EditTool, glob::GlobTool,
        grep::GrepTool, http::HttpClientTool, load_brain_file::LoadBrainFileTool, ls::LsTool,
        memory_search::MemorySearchTool, notebook::NotebookEditTool,
        pdf_to_images::PdfToImagesTool, plan_tool::PlanTool, read::ReadTool,
        rename_session::RenameSessionTool, session_search::SessionSearchTool,
        slash_command::SlashCommandTool, suggest_options::SuggestOptionsTool,
        web_search::WebSearchTool, write::WriteTool, write_opencrabs_file::WriteOpenCrabsFileTool,
    };
    // Phase 1: Essential file operations
    tool_registry.register(Arc::new(ReadTool));
    tool_registry.register(Arc::new(WriteTool));
    tool_registry.register(Arc::new(EditTool));
    tool_registry.register(Arc::new(crate::brain::tools::hashline::HashlineEditTool));
    tool_registry.register(Arc::new(BashTool));
    tool_registry.register(Arc::new(LsTool));
    tool_registry.register(Arc::new(GlobTool));
    tool_registry.register(Arc::new(GrepTool));
    // Phase 2: Advanced features
    tool_registry.register(Arc::new(WebSearchTool::default()));
    tool_registry.register(Arc::new(CodeExecTool));
    tool_registry.register(Arc::new(NotebookEditTool));
    tool_registry.register(Arc::new(DocParserTool));
    tool_registry.register(Arc::new(GenerateDocumentTool));
    tool_registry.register(Arc::new(PdfToImagesTool));
    // Phase 3: Workflow & integration
    // task_manager is deprecated in favor of the plan tool (mode=checklist)
    // and no longer registered, so it leaves the model-facing catalog and
    // tool_search. The TaskTool code stays in place (brain::tools::task) for
    // reference and any out-of-band callers.
    tool_registry.register(Arc::new(ContextTool));
    tool_registry.register(Arc::new(HttpClientTool));
    tool_registry.register(Arc::new(PlanTool));
    // Memory search (built-in FTS5, always available)
    tool_registry.register(Arc::new(MemorySearchTool));
    // On-demand brain file loader — agent fetches USER.md, MEMORY.md etc. only when needed
    tool_registry.register(Arc::new(LoadBrainFileTool));
    // OpenCrabs file writer — agent can edit/append/overwrite any file in ~/.opencrabs/
    tool_registry.register(Arc::new(WriteOpenCrabsFileTool));
    // Session search — hybrid QMD search across all session message history
    tool_registry.register(Arc::new(SessionSearchTool::new(db.pool().clone())));
    // Mission control report — shareable analytics/activity/inbox/schedule the agent can send to a chat
    tool_registry.register(Arc::new(
        crate::brain::tools::mission_control_report::MissionControlReportTool::new(
            db.pool().clone(),
        ),
    ));
    // Channel search — search passively captured channel messages (Telegram groups, etc.)
    tool_registry.register(Arc::new(
        crate::brain::tools::channel_search::ChannelSearchTool::new(
            crate::db::ChannelMessageRepository::new(db.pool().clone()),
        ),
    ));
    // Cron job management — agent can create/list/delete/enable/disable scheduled jobs
    tool_registry.register(Arc::new(
        crate::brain::tools::cron_manage::CronManageTool::new(crate::db::CronJobRepository::new(
            db.pool().clone(),
        )),
    ));
    // Goal management — agent can set/manage its own autonomous session goal,
    // so it can drive multi-turn test/review/fix cycles instead of waiting for
    // the human to type /goal. The post-turn judge already activates on any
    // active goal row, tool-set or slash-set alike.
    tool_registry.register(Arc::new(crate::brain::tools::goal_manage::GoalManageTool));
    // A2A send — agent can communicate with remote A2A agents
    tool_registry.register(Arc::new(crate::brain::tools::a2a_send::A2aSendTool::new()));
    tool_registry.register(Arc::new(
        crate::brain::tools::profile_list::ProfileListTool::new(),
    ));
    // Tasks list — one roster of sub-agents + detached commands (#1160)
    tool_registry.register(Arc::new(
        crate::brain::tools::tasks_list::TasksListTool::new(),
    ));
    // Config management (read/write config.toml, commands.toml)
    tool_registry.register(Arc::new(ConfigTool));
    // Slash command invocation (agent can call any slash command)
    tool_registry.register(Arc::new(SlashCommandTool));
    // Session rename — agent can update the current session's title
    tool_registry.register(Arc::new(RenameSessionTool));
    // #129: interactive-surface-only tools. On headless paths (CLI one-shot
    // run, cron daemon execute, sub-agent spawn) neither tool can reach a
    // user: session_notify parks until a turn-end relay that never fires on
    // a one-shot process, and suggest_options renders nowhere. Skipped at
    // registration; the ToolExecutionContext.headless guard backstops.
    if !headless {
        // Follow-up question — agent asks the user a multi-choice question
        // mid-task and blocks until they click an option button.
        tool_registry.register(Arc::new(SuggestOptionsTool));
    }
    // Tool discovery — lets the agent activate extended tools on demand
    // (lazy-tools mode). Holds the registry Arc so it can search all tools;
    // harmless when lazy_tools is off (just one more always-available tool).
    tool_registry.register(Arc::new(
        crate::brain::tools::tool_search::ToolSearchTool::new(tool_registry),
    ));
    // Tools whose availability depends on config / keys (EXA, Brave, image
    // generation, vision/video). Shared with the config watcher so a key added
    // to keys.toml is picked up at runtime with no restart, in the daemon too.
    super::ui::register_config_dependent_tools(tool_registry, config);

    // Phase 5: Multi-agent orchestration
    let subagent_manager = Arc::new(crate::brain::tools::subagent::SubAgentManager::new());
    tool_registry.register(Arc::new(
        crate::brain::tools::subagent::SpawnAgentTool::new(
            subagent_manager.clone(),
            tool_registry.clone(),
        ),
    ));
    tool_registry.register(Arc::new(crate::brain::tools::subagent::WaitAgentTool::new(
        subagent_manager.clone(),
    )));
    tool_registry.register(Arc::new(crate::brain::tools::subagent::SendInputTool::new(
        subagent_manager.clone(),
    )));
    tool_registry.register(Arc::new(
        crate::brain::tools::subagent::CloseAgentTool::new(subagent_manager.clone()),
    ));
    tool_registry.register(Arc::new(
        crate::brain::tools::subagent::ResumeAgentTool::new(
            subagent_manager.clone(),
            tool_registry.clone(),
        ),
    ));
    // Cross-session push (issue #1203): needs no manager — deliver_to_session
    // is a free function on the in-process session-route registry.
    // #129: interactive-only — parked notifications die with the process on
    // headless paths (owner ruling C: sub-agents report via the harness's
    // final-message relay, proven live-fire 2026-09-07). Gated with
    // suggest_options above; the context guard backstops both.
    if !headless {
        tool_registry.register(Arc::new(crate::brain::tools::subagent::SessionNotifyTool));
    }

    // Phase 6: Team orchestration
    let team_manager = Arc::new(crate::brain::tools::subagent::TeamManager::new());
    tool_registry.register(Arc::new(
        crate::brain::tools::subagent::TeamCreateTool::new(
            subagent_manager.clone(),
            team_manager.clone(),
            tool_registry.clone(),
        ),
    ));
    tool_registry.register(Arc::new(
        crate::brain::tools::subagent::TeamDeleteTool::new(
            subagent_manager.clone(),
            team_manager.clone(),
        ),
    ));
    tool_registry.register(Arc::new(
        crate::brain::tools::subagent::TeamBroadcastTool::new(
            subagent_manager.clone(),
            team_manager.clone(),
        ),
    ));
    tracing::info!("Registered 9 sub-agent + team orchestration tools");

    // Recursive Self-Improvement tools
    tool_registry.register(Arc::new(
        crate::brain::tools::feedback_record::FeedbackRecordTool,
    ));
    tool_registry.register(Arc::new(
        crate::brain::tools::feedback_analyze::FeedbackAnalyzeTool,
    ));
    tool_registry.register(Arc::new(crate::brain::tools::self_improve::SelfImproveTool));
    tracing::info!("Registered 3 recursive self-improvement tools");

    subagent_manager
}

/// Register the headless-safe RUNTIME tools that depend only on config + the
/// registry itself (never on live TUI or channel state): user-defined dynamic
/// tools from `tools.toml`, the dynamic-tool manager, and browser automation.
///
/// Every entry point that runs an agent calls this on top of
/// [`register_core_agent_tools`] so the tool set is identical across the TUI,
/// the multi-profile cron daemon, and the one-shot/headless CLI paths. Without
/// it, a cron job under a secondary profile (or a `run`/agent CLI invocation)
/// could not use the user's own `tools.toml` tools or the browser.
///
/// Deliberately EXCLUDED here (and registered only by the interactive path):
/// - **Channel send/connect tools** (`telegram_send`, `*_connect`, ...): they
///   hold live in-process channel state (`TelegramState` etc.) that only exists
///   once a bot is connected, which the daemon never does. Registering them
///   would yield dead tools that always answer "not connected". Cron jobs reach
///   channels through the job's config-based `deliver_to` path instead, which
///   builds its own client from the API key and needs no live state.
/// - **evolve / rebuild**: binary self-update is owned by the primary daemon;
///   letting concurrent secondary profiles self-update would race on one binary.
pub(crate) fn register_runtime_tools(tool_registry: &Arc<ToolRegistry>, config: &Config) {
    // User-defined dynamic tools from the profile-aware ~/.opencrabs/tools.toml.
    let tools_toml_path = crate::brain::tools::dynamic::DynamicToolLoader::default_path()
        .unwrap_or_else(|| std::path::PathBuf::from("tools.toml"));
    let dynamic_count =
        crate::brain::tools::dynamic::DynamicToolLoader::load(&tools_toml_path, tool_registry);
    if dynamic_count > 0 {
        tracing::info!("Loaded {dynamic_count} dynamic tool(s) from tools.toml");
    }
    // tool_manage — agent can add/remove/reload dynamic tools at runtime.
    tool_registry.register(Arc::new(
        crate::brain::tools::tool_manage::ToolManageTool::new(
            tool_registry.clone(),
            tools_toml_path,
        ),
    ));

    // Browser automation (headless Chrome via CDP). Config-only construction and
    // lazy launch, so it is safe to register everywhere — chrome is spawned on
    // first use, not here.
    #[cfg(feature = "browser")]
    {
        let browser_manager = Arc::new(crate::brain::tools::browser::BrowserManager::new(
            config.browser.clone(),
        ));
        tool_registry.register(Arc::new(
            crate::brain::tools::browser::BrowserNavigateTool::new(browser_manager.clone()),
        ));
        tool_registry.register(Arc::new(
            crate::brain::tools::browser::BrowserScreenshotTool::new(browser_manager.clone()),
        ));
        tool_registry.register(Arc::new(
            crate::brain::tools::browser::BrowserClickTool::new(browser_manager.clone()),
        ));
        tool_registry.register(Arc::new(
            crate::brain::tools::browser::BrowserTypeTool::new(browser_manager.clone()),
        ));
        tool_registry.register(Arc::new(
            crate::brain::tools::browser::BrowserEvalTool::new(browser_manager.clone()),
        ));
        tool_registry.register(Arc::new(
            crate::brain::tools::browser::BrowserContentTool::new(browser_manager.clone()),
        ));
        tool_registry.register(Arc::new(
            crate::brain::tools::browser::BrowserWaitTool::new(browser_manager.clone()),
        ));
        tool_registry.register(Arc::new(
            crate::brain::tools::browser::BrowserFindTool::new(browser_manager.clone()),
        ));
        tool_registry.register(Arc::new(crate::brain::tools::browser::BrowserActTool::new(
            browser_manager.clone(),
        )));
        // web_scrape escalates JS-only pages to a headless render, so it takes a
        // clone of the browser manager here (kept out of CORE_TOOLS, surfaced via
        // tool_search). Registered before BrowserCloseTool consumes the manager.
        tool_registry.register(Arc::new(
            crate::brain::tools::web_scrape::WebScrapeTool::default()
                .with_browser(browser_manager.clone()),
        ));
        tool_registry.register(Arc::new(
            crate::brain::tools::browser::BrowserCloseTool::new(browser_manager),
        ));
        tracing::info!("Browser automation tools registered (10 tools)");
    }
    #[cfg(not(feature = "browser"))]
    {
        let _ = config;
        // No browser to escalate JS shells; web_scrape still serves static pages.
        tool_registry.register(Arc::new(
            crate::brain::tools::web_scrape::WebScrapeTool::default(),
        ));
    }
}

/// Headless-only preamble (#129, owner-ruled design): appended to the system
/// brain on headless paths ONLY (CLI one-shot `run`, cron daemon execute,
/// sub-agent spawn). The user cannot see a headless session's intermediate
/// messages — only the final one — so the agent must make that final message
/// self-contained instead of narrating progress nobody receives.
///
/// Deliberately NOT in AGENTS.md: that file is always-loaded on every surface
/// (TUI, channels), where the directive is wrong — there, intermediate
/// messages ARE visible and narrating progress is correct behaviour. A
/// brain-only rule cannot be headless-scoped; this preamble can.
pub const HEADLESS_PREAMBLE: &str = "\n\n## Headless session\n\nYou are running HEADLESS: the user cannot see your intermediate messages, tool calls, or progress updates — they receive ONLY your final message. That final message must be SELF-CONTAINED: state what was done with the concrete specifics (commit shas, file paths, test results, numbers), anything that failed and why, and what is needed next. Do not ask questions mid-task (no answer channel exists); make the reasonable assumption, state it, finish. Do not use `session_notify` or `suggest_options` — neither can reach a user from here. If you cannot complete the task, your final message is the only report: say what blocked you with the evidence.\n";