use crate::app::agent::approval::ApprovalManager;
use crate::app::agent::config::Config;
use crate::app::agent::memory::{self, Memory};
use crate::app::agent::observability::{self, Observer, ObserverEvent};
use crate::app::agent::providers::Provider;
use crate::app::agent::runtime;
use crate::app::agent::security::SecurityPolicy;
use anyhow::Result;
use std::sync::Arc;
use crate::app::agent::agent::loop_::instructions::{
build_shell_policy_instructions, build_tool_instructions,
};
pub(crate) struct CliSetup {
pub(crate) observer: Arc<dyn Observer>,
pub(crate) mem: Arc<dyn Memory>,
pub(crate) provider: Box<dyn Provider>,
pub(crate) tools_registry: Vec<Box<dyn crate::app::agent::tools::Tool>>,
pub(crate) provider_name: String,
pub(crate) model_name: String,
pub(crate) system_prompt: String,
pub(crate) approval_manager: Option<ApprovalManager>,
pub(crate) channel_name: &'static str,
}
pub(crate) fn setup_cli(
config: &Config,
interactive: bool,
provider_override: Option<&str>,
model_override: Option<&str>,
) -> Result<CliSetup> {
let base_observer = observability::create_observer(&config.observability);
let observer: Arc<dyn Observer> = Arc::from(base_observer);
let runtime: Arc<dyn runtime::RuntimeAdapter> =
Arc::from(runtime::create_runtime(&config.runtime)?);
let security = Arc::new(SecurityPolicy::from_config(&config.autonomy, &config.workspace_dir));
let mem: Arc<dyn Memory> = Arc::from(memory::create_memory_with_storage(
&config.memory,
Some(&config.storage.provider.config),
&config.workspace_dir,
config.api_key.as_deref(),
)?);
tracing::info!(backend = mem.name(), "Memory initialized");
let (composio_key, composio_entity_id) = if config.composio.enabled {
(config.composio.api_key.as_deref(), Some(config.composio.entity_id.as_str()))
} else {
(None, None)
};
let tools_registry = crate::app::agent::tools::all_tools_with_runtime(
Arc::new(config.clone()), &security, runtime, mem.clone(), composio_key, composio_entity_id, &config.browser, &config.http_request, &config.web_fetch, &config.workspace_dir, &config.agents, config.api_key.as_deref(), config, Some("cli"), );
let provider_name = provider_override
.or(config.default_provider.as_deref())
.unwrap_or("zhipuai-coding-plan")
.to_string();
let model_name = model_override
.or(config.default_model.as_deref())
.unwrap_or("zhipuai-coding-plan/glm-4.7")
.to_string();
let provider_runtime_options = crate::app::agent::providers::ProviderRuntimeOptions {
auth_profile_override: None, provider_api_url: config.api_url.clone(), vibewindow_dir: config.config_path.parent().map(std::path::PathBuf::from), secrets_encrypt: config.secrets.encrypt, reasoning_enabled: config.runtime.reasoning_enabled, reasoning_level: config.effective_provider_reasoning_level(), custom_provider_api_mode: config.provider_api.map(|mode| mode.as_compatible_mode()), max_tokens_override: None, model_support_vision: config.model_support_vision, };
let provider: Box<dyn Provider> =
crate::app::agent::providers::create_routed_provider_with_options(
&provider_name,
config.api_key.as_deref(),
config.api_url.as_deref(),
&config.reliability,
&config.model_routes,
&model_name,
&provider_runtime_options,
)?;
observer.record_event(&ObserverEvent::AgentStart {
provider: provider_name.to_string(),
model: model_name.to_string(),
});
let skills = crate::app::agent::skills::load_skills_with_config(&config.workspace_dir, config);
let mut tool_descs: Vec<(&str, &str)> = vec![
(
"shell",
"Execute terminal commands. Use when: running local checks, build/test commands, diagnostics. Don't use when: a safer dedicated tool exists, or command is destructive without approval.",
),
(
"file_read",
"Read file contents. Use when: inspecting project files, configs, logs. Don't use when: a targeted search is enough.",
),
(
"file_write",
"Write file contents. Use when: applying focused edits, scaffolding files, updating docs/code. Don't use when: side effects are unclear or file ownership is uncertain.",
),
(
"memory_store",
"Save to memory. Use when: preserving durable preferences, decisions, key context. Don't use when: information is transient/noisy/sensitive without need.",
),
(
"memory_recall",
"Search memory. Use when: retrieving prior decisions, user preferences, historical context. Don't use when: answer is already in current context.",
),
(
"memory_forget",
"Delete a memory entry. Use when: memory is incorrect/stale or explicitly requested for removal. Don't use when: impact is uncertain.",
),
];
tool_descs.push((
"cron_add",
"Create a cron job. Supports schedule kinds: cron, at, every; and job types: shell or agent.",
));
tool_descs.push(("cron_list", "List all cron jobs with schedule, status, and metadata."));
tool_descs.push(("cron_remove", "Remove a cron job by job_id."));
tool_descs.push((
"cron_update",
"Patch a cron job (schedule, enabled, command/prompt, model, delivery, session_target).",
));
tool_descs
.push(("cron_run", "Force-run a cron job immediately and record a run history entry."));
tool_descs.push(("cron_runs", "Show recent run history for a cron job."));
tool_descs.push((
"screenshot",
"Capture a screenshot of the current screen. Returns file path and base64-encoded PNG. Use when: visual verification, UI inspection, debugging displays.",
));
tool_descs.push((
"image_info",
"Read image file metadata (format, dimensions, size) and optionally base64-encode it. Use when: inspecting images, preparing visual data for analysis.",
));
if config.browser.enabled {
tool_descs.push((
"browser_open",
"Open approved HTTPS URLs in system browser (allowlist-only, no scraping)",
));
}
if config.composio.enabled {
tool_descs.push((
"composio",
"Execute actions on 1000+ apps via Composio (Gmail, Notion, GitHub, Slack, etc.). Use action='list' to discover, 'execute' to run (optionally with connected_account_id), 'connect' to OAuth.",
));
}
tool_descs.push((
"schedule",
"Manage scheduled tasks (create/list/get/cancel/pause/resume). Supports recurring cron and one-shot delays.",
));
tool_descs.push((
"model_routing_config",
"Configure default model, scenario routing, and delegate agents. Use for natural-language requests like: 'set conversation to kimi and coding to gpt-5.3-codex'.",
));
if tools_registry.iter().any(|tool| tool.name() == "AgentTool") {
tool_descs.push((
"AgentTool",
"Launch a specialized agent through a single unified interface. Use it for synchronous sub-agent execution or background agent sessions, and use action=list/get/stop to inspect or control running sessions.",
));
}
let bootstrap_max_chars = if config.agent.compact_context { Some(6000) } else { None };
let native_tools = provider.supports_native_tools();
let mut system_prompt = crate::app::agent::channels::build_system_prompt_with_mode(
&config.workspace_dir,
&model_name,
&tool_descs,
&skills,
Some(&config.identity),
bootstrap_max_chars,
native_tools,
config.skills.prompt_injection_mode,
);
if !native_tools {
system_prompt.push_str(&build_tool_instructions(&tools_registry));
}
system_prompt.push_str(&build_shell_policy_instructions(&config.autonomy));
let approval_manager =
if interactive { Some(ApprovalManager::from_config(&config.autonomy)) } else { None };
let channel_name = if interactive { "cli" } else { "daemon" };
Ok(CliSetup {
observer,
mem,
provider,
tools_registry,
provider_name,
model_name,
system_prompt,
approval_manager,
channel_name,
})
}