use std::path::Path;
use anyhow::Result;
use colored::Colorize as _;
use oxo_flow_ai::agent::orchestrator::Orchestrator;
use oxo_flow_ai::config::AiConfig;
use oxo_flow_ai::provider::AiProvider;
use oxo_flow_ai::tools::{ToolRegistry, builtin};
pub struct AiRuntime {
pub provider: AiProvider,
pub config: AiConfig,
pub tool_registry: ToolRegistry,
pub orchestrator: Orchestrator,
pub skill_context: String,
}
impl AiRuntime {
pub async fn new(
workflow_path: Option<&Path>,
project_dir: Option<&Path>,
cli_max_retries: Option<u32>,
) -> Result<Self> {
let provider = super::ai_template::resolve_ai_provider()?;
let global = load_global_config();
let project = project_dir.and_then(AiConfig::from_project_file);
let workflow = workflow_path.and_then(|p| {
std::fs::read_to_string(p).ok().and_then(|c| {
c.parse::<toml::Table>()
.ok()
.and_then(|t| AiConfig::from_workflow_toml(&t))
})
});
let cli_overrides = cli_max_retries.map(|n| AiConfig {
max_retries: n,
..Default::default()
});
let config = AiConfig::resolve_chain(
global.as_ref(),
project.as_ref(),
workflow.as_ref(),
None,
cli_overrides.as_ref(),
);
let mut tool_registry = ToolRegistry::new();
tool_registry.register(Box::new(builtin::ReadFileTool::new()));
tool_registry.register(Box::new(builtin::FetchUrlTool::new()));
tool_registry.register(Box::new(builtin::LookupTool::new()));
tool_registry.register(Box::new(builtin::LookupSkillTool::new()));
tool_registry.register(Box::new(builtin::LookupPipelineTool::new()));
let project = project_dir.or_else(|| workflow_path.and_then(|p| p.parent()));
let skill_context = activated_skill_context(project, &config);
let discovered = oxo_flow_ai::skill::discover_skills(project);
let mut mcp_tools = 0usize;
for skill in discovered {
if skill.skill_type != "tool" || !config.skills.iter().any(|name| name == &skill.name) {
continue;
}
let Some(url) = skill
.requires
.as_ref()
.and_then(|r| r.iter().find(|s| s.starts_with("mcp://")))
else {
tracing::warn!(
"tool skill '{}' has no mcp:// requirement — skipped",
skill.name
);
continue;
};
match oxo_flow_ai::mcp::McpHttpClient::new(url) {
Ok(client) => {
match oxo_flow_ai::mcp::McpToolBridge::discover(std::sync::Arc::new(client))
.await
{
Ok(bridges) => {
let count = bridges.len();
for bridge in bridges {
tool_registry.register(Box::new(bridge));
}
mcp_tools += count;
tracing::info!(
"skill '{}': registered {count} MCP tool(s) from {url}",
skill.name
);
}
Err(e) => tracing::warn!(
"skill '{}': failed to discover MCP tools at {url}: {e}",
skill.name
),
}
}
Err(e) => tracing::warn!("skill '{}': {e}", skill.name),
}
}
if mcp_tools > 0 {
eprintln!(
" {} {} MCP tool(s) from activated tool skill(s) (non-read-only calls require approval)",
"•".cyan(),
mcp_tools
);
}
let orchestrator = Orchestrator::new(provider.clone(), config.max_retries);
Ok(Self {
provider,
config,
tool_registry,
orchestrator,
skill_context,
})
}
}
fn load_global_config() -> Option<AiConfig> {
let path = oxo_flow_ai::provider::ai_config_path();
let content = std::fs::read_to_string(&path).ok()?;
let json: serde_json::Value = serde_json::from_str(&content).ok()?;
let provider_str = json["provider"].as_str().unwrap_or("");
let provider: oxo_flow_ai::provider::ProviderKind = provider_str.parse().ok()?;
Some(AiConfig {
enabled: true,
provider,
model: json["model"].as_str().map(String::from),
api_url: json["api_url"].as_str().map(String::from),
..Default::default()
})
}
pub fn activated_skill_context(project_dir: Option<&Path>, config: &AiConfig) -> String {
let discovered = oxo_flow_ai::skill::discover_skills(project_dir);
let mut registry = oxo_flow_ai::skill::SkillRegistry::new();
for skill in discovered {
if config.skills.iter().any(|name| name == &skill.name) {
tracing::info!("Activated custom skill: {} v{}", skill.name, skill.version);
registry.activate(skill);
}
}
registry.prompt_context().to_string()
}
pub async fn prompt_tool_approval(tool_name: &str, arguments: &str) -> bool {
let can_prompt = std::io::IsTerminal::is_terminal(&std::io::stderr())
&& std::io::IsTerminal::is_terminal(&std::io::stdin());
if !can_prompt {
return false;
}
eprintln!();
eprintln!(" {} tool '{}' is not read-only:", "⚠".yellow(), tool_name);
eprintln!(" Arguments: {arguments}");
eprint!(" Allow execution? [y/N] ");
use std::io::Write as _;
std::io::stderr().flush().ok();
tokio::task::spawn_blocking(|| {
let mut input = String::new();
if std::io::stdin().read_line(&mut input).is_ok() {
let trimmed = input.trim();
trimmed.eq_ignore_ascii_case("y") || trimmed.eq_ignore_ascii_case("yes")
} else {
false
}
})
.await
.unwrap_or(false)
}