use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use uira_core::UIRA_DIR;
use super::agent::{AgentDefinitionEntry, AgentDefinitions};
use super::config::PluginConfig;
use super::mcp::McpServerConfig;
use super::types::{AgentState, BackgroundTask};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SessionOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub config: Option<PluginConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub working_directory: Option<String>,
#[serde(default)]
pub skip_config_load: bool,
#[serde(default)]
pub skip_context_injection: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub custom_system_prompt: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub api_key: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct SessionState {
pub session_id: Option<String>,
pub active_agents: HashMap<String, AgentState>,
pub background_tasks: Vec<BackgroundTask>,
pub context_files: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryOptions {
pub system_prompt: String,
pub agents: AgentDefinitions,
pub mcp_servers: HashMap<String, McpServerConfig>,
pub allowed_tools: Vec<String>,
pub permission_mode: String,
}
#[derive(Debug, Clone)]
pub struct UiraSession {
pub query_options: QueryOptions,
pub state: SessionState,
pub config: PluginConfig,
}
impl UiraSession {
pub fn new(options: SessionOptions) -> Self {
let working_dir = options
.working_directory
.as_ref()
.map(PathBuf::from)
.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
let config = if options.skip_config_load {
options.config.unwrap_or_default()
} else {
Self::load_and_merge_config(&working_dir, options.config)
};
let mut system_prompt = Self::get_default_system_prompt();
if config
.features
.as_ref()
.and_then(|f| f.continuation_enforcement)
.unwrap_or(true)
{
system_prompt.push_str(&Self::get_continuation_prompt());
}
if let Some(custom) = options.custom_system_prompt {
system_prompt.push_str("\n\n## Custom Instructions\n\n");
system_prompt.push_str(&custom);
}
let agents = Self::get_default_agent_definitions();
let mcp_servers = Self::get_default_mcp_servers(&config);
let mut allowed_tools = vec![
"Read".to_string(),
"Glob".to_string(),
"Grep".to_string(),
"WebSearch".to_string(),
"WebFetch".to_string(),
"Task".to_string(),
"TodoWrite".to_string(),
];
if config
.permissions
.as_ref()
.and_then(|p| p.allow_bash)
.unwrap_or(true)
{
allowed_tools.push("Bash".to_string());
}
if config
.permissions
.as_ref()
.and_then(|p| p.allow_edit)
.unwrap_or(true)
{
allowed_tools.push("Edit".to_string());
}
if config
.permissions
.as_ref()
.and_then(|p| p.allow_write)
.unwrap_or(true)
{
allowed_tools.push("Write".to_string());
}
for server_name in mcp_servers.keys() {
allowed_tools.push(format!("mcp__{}__*", server_name));
}
let context_files = if options.skip_context_injection {
Vec::new()
} else {
Self::find_context_files(&working_dir)
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect()
};
let state = SessionState {
session_id: None,
active_agents: HashMap::new(),
background_tasks: Vec::new(),
context_files,
};
let query_options = QueryOptions {
system_prompt,
agents,
mcp_servers,
allowed_tools,
permission_mode: "acceptEdits".to_string(),
};
Self {
query_options,
state,
config,
}
}
fn get_default_system_prompt() -> String {
r#"You are Uira, a multi-agent orchestration system.
You are the orchestrator that coordinates specialized agents to accomplish complex tasks.
You have access to specialized agents that you can delegate tasks to using the Task tool.
## Available Agents
- **explore**: Fast codebase search using grep, glob, and LSP
- **architect**: Architecture and debugging advisor
- **executor**: Focused task executor for implementing changes
## Your Role
1. Analyze user requests and break them into subtasks
2. Delegate subtasks to appropriate specialized agents
3. Coordinate agent outputs to produce final results
4. Ensure all work is verified and complete before finishing
Use the Task tool to invoke agents for specific purposes."#
.to_string()
}
fn get_continuation_prompt() -> String {
r#"
## Continuation Enforcement
You MUST complete all tasks before stopping. If you have pending todos or incomplete work,
continue working until everything is done."#
.to_string()
}
fn load_and_merge_config(
working_dir: &Path,
options_config: Option<PluginConfig>,
) -> PluginConfig {
let loaded_config = Self::find_and_load_config(working_dir);
match (loaded_config, options_config) {
(Some(loaded), Some(opts)) => Self::merge_configs(loaded, opts),
(Some(loaded), None) => loaded,
(None, Some(opts)) => opts,
(None, None) => PluginConfig::default(),
}
}
fn find_and_load_config(working_dir: &Path) -> Option<PluginConfig> {
let config_paths = Self::find_config_files(working_dir);
for path in config_paths {
if path.exists() {
if let Ok(content) = std::fs::read_to_string(&path) {
if let Ok(config) = serde_yaml_ng::from_str::<PluginConfig>(&content) {
return Some(config);
}
if let Ok(config) = serde_json::from_str::<PluginConfig>(&content) {
return Some(config);
}
if let Ok(uira_config) =
serde_yaml_ng::from_str::<uira_core::UiraConfig>(&content)
{
return Some(Self::convert_uira_to_plugin_config(&uira_config));
}
if let Ok(uira_config) = serde_json::from_str::<uira_core::UiraConfig>(&content)
{
return Some(Self::convert_uira_to_plugin_config(&uira_config));
}
}
}
}
None
}
fn convert_uira_to_plugin_config(uira: &uira_core::UiraConfig) -> PluginConfig {
use super::config::*;
let agents = if !uira.agents.agents.is_empty() {
Some(AgentsConfig::default())
} else {
None
};
let mcp_servers = if !uira.mcp.servers.is_empty() {
let mut config = McpServersConfig::default();
if let Some(exa) = uira.mcp.get("exa") {
config.exa = Some(ExaConfig {
enabled: Some(true),
api_key: exa.env.get("EXA_API_KEY").cloned(),
});
}
if uira.mcp.contains_key("context7") {
config.context7 = Some(Context7Config {
enabled: Some(true),
});
}
Some(config)
} else {
None
};
PluginConfig {
agents,
features: Some(FeaturesConfig {
parallel_execution: Some(true),
lsp_tools: Some(false),
ast_tools: Some(false),
continuation_enforcement: Some(true),
auto_context_injection: Some(true),
}),
mcp_servers,
permissions: Some(PermissionsConfig {
allow_bash: Some(true),
allow_edit: Some(true),
allow_write: Some(true),
max_background_tasks: Some(5),
}),
magic_keywords: None,
routing: None,
}
}
fn find_config_files(working_dir: &Path) -> Vec<PathBuf> {
let mut paths = Vec::new();
let local_candidates = vec![
"uira.yaml",
"uira.yml",
"uira.json",
".uira.yaml",
".uira.yml",
];
for candidate in &local_candidates {
let path = working_dir.join(candidate);
if path.exists() {
paths.push(path);
}
}
if let Ok(home) = std::env::var("HOME") {
let home_path = PathBuf::from(home);
let uira_config = home_path.join(UIRA_DIR).join("config.yaml");
if uira_config.exists() {
paths.push(uira_config);
}
let xdg_config = home_path.join(".config").join("uira").join("config.yaml");
if xdg_config.exists() {
paths.push(xdg_config);
}
}
paths
}
fn find_context_files(working_dir: &Path) -> Vec<PathBuf> {
let mut context_files = Vec::new();
let candidates = vec![
"CLAUDE.md",
"AGENTS.md",
".claude/CLAUDE.md",
".claude/AGENTS.md",
];
for candidate in &candidates {
let path = working_dir.join(candidate);
if path.exists() && path.is_file() {
context_files.push(path);
}
}
context_files
}
fn merge_configs(base: PluginConfig, opts: PluginConfig) -> PluginConfig {
PluginConfig {
agents: opts.agents.or(base.agents),
features: opts.features.or(base.features),
mcp_servers: opts.mcp_servers.or(base.mcp_servers),
permissions: opts.permissions.or(base.permissions),
magic_keywords: opts.magic_keywords.or(base.magic_keywords),
routing: opts.routing.or(base.routing),
}
}
fn get_default_agent_definitions() -> AgentDefinitions {
let mut agents = HashMap::new();
agents.insert(
"explore".to_string(),
AgentDefinitionEntry {
description: "Fast codebase search using grep, glob, and LSP".to_string(),
prompt: "You are an explore agent. Search the codebase efficiently.".to_string(),
tools: vec!["Read".to_string(), "Glob".to_string(), "Grep".to_string()],
model: Some("haiku".to_string()),
},
);
agents.insert(
"architect".to_string(),
AgentDefinitionEntry {
description: "Architecture and debugging advisor".to_string(),
prompt: "You are an architect agent. Provide strategic guidance.".to_string(),
tools: vec![
"Read".to_string(),
"Glob".to_string(),
"Grep".to_string(),
"WebSearch".to_string(),
],
model: Some("opus".to_string()),
},
);
agents.insert(
"executor".to_string(),
AgentDefinitionEntry {
description: "Focused task executor".to_string(),
prompt: "You are an executor agent. Implement changes directly.".to_string(),
tools: vec![
"Read".to_string(),
"Glob".to_string(),
"Grep".to_string(),
"Edit".to_string(),
"Write".to_string(),
"Bash".to_string(),
],
model: Some("sonnet".to_string()),
},
);
agents
}
fn get_default_mcp_servers(config: &PluginConfig) -> HashMap<String, McpServerConfig> {
let mut servers = HashMap::new();
if config
.mcp_servers
.as_ref()
.and_then(|m| m.context7.as_ref())
.and_then(|c| c.enabled)
.unwrap_or(true)
{
servers.insert(
"context7".to_string(),
McpServerConfig {
command: "npx".to_string(),
args: vec!["-y".to_string(), "@upstash/context7-mcp".to_string()],
env: None,
},
);
}
if let Some(api_key) = config
.mcp_servers
.as_ref()
.and_then(|m| m.exa.as_ref())
.and_then(|e| e.api_key.clone())
{
let mut env = HashMap::new();
env.insert("EXA_API_KEY".to_string(), api_key);
servers.insert(
"exa".to_string(),
McpServerConfig {
command: "npx".to_string(),
args: vec!["-y".to_string(), "exa-mcp-server".to_string()],
env: Some(env),
},
);
}
servers
}
pub fn process_prompt(&self, prompt: &str) -> String {
let lowercase = prompt.to_lowercase();
let keywords = &[
("autopilot", "[AUTOPILOT MODE ACTIVATED]"),
("ultrawork", "[ULTRAWORK MODE ACTIVATED]"),
("ulw", "[ULTRAWORK MODE ACTIVATED]"),
("ralph", "[RALPH MODE ACTIVATED]"),
("plan", "[PLANNING MODE]"),
("ecomode", "[ECOMODE ACTIVATED]"),
("eco", "[ECOMODE ACTIVATED]"),
("ultrapilot", "[ULTRAPILOT MODE ACTIVATED]"),
("ralplan", "[RALPLAN MODE ACTIVATED]"),
];
for (keyword, prefix) in keywords {
if lowercase.contains(keyword) {
let is_enabled = match *keyword {
"ultrawork" | "ulw" => {
self.config
.magic_keywords
.as_ref()
.and_then(|k| k.ultrawork.as_ref())
.map(|keywords| {
keywords
.iter()
.any(|k| lowercase.contains(&k.to_lowercase()))
})
.unwrap_or(true) }
"plan" => {
self.config
.magic_keywords
.as_ref()
.and_then(|k| k.search.as_ref())
.map(|keywords| {
keywords
.iter()
.any(|k| lowercase.contains(&k.to_lowercase()))
})
.unwrap_or(true) }
"analyze" => {
self.config
.magic_keywords
.as_ref()
.and_then(|k| k.analyze.as_ref())
.map(|keywords| {
keywords
.iter()
.any(|k| lowercase.contains(&k.to_lowercase()))
})
.unwrap_or(true) }
_ => true, };
if is_enabled {
return format!("{}\n\n{}", prefix, prompt);
}
}
}
prompt.to_string()
}
pub fn detect_keywords(&self, prompt: &str) -> Vec<String> {
let mut keywords = Vec::new();
let prompt_lower = prompt.to_lowercase();
if prompt_lower.contains("ultrawork") || prompt_lower.contains("ulw") {
keywords.push("ultrawork".to_string());
}
if prompt_lower.contains("ralph") {
keywords.push("ralph".to_string());
}
if prompt_lower.contains("plan") {
keywords.push("plan".to_string());
}
keywords
}
}
pub fn create_uira_session(options: Option<SessionOptions>) -> UiraSession {
UiraSession::new(options.unwrap_or_default())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_session_default() {
let session = create_uira_session(None);
assert!(!session.query_options.system_prompt.is_empty());
assert!(!session.query_options.agents.is_empty());
assert!(session
.query_options
.allowed_tools
.contains(&"Read".to_string()));
}
#[test]
fn test_session_with_custom_prompt() {
let options = SessionOptions {
custom_system_prompt: Some("Always be helpful.".to_string()),
..Default::default()
};
let session = create_uira_session(Some(options));
assert!(session
.query_options
.system_prompt
.contains("Always be helpful"));
}
#[test]
fn test_detect_keywords() {
let session = create_uira_session(None);
let keywords = session.detect_keywords("ultrawork: fix the bug");
assert!(keywords.contains(&"ultrawork".to_string()));
}
}