pub mod core;
pub mod providers;
use crate::config::{Config, McpServerConfig, McpTransport, OperatorConfig};
use directories::UserDirs;
use std::collections::HashMap;
pub const OPERATOR_SERVER_NAME: &str = "construct-operator";
pub const DEFAULT_OPERATOR_MCP_PATH_SUFFIX: &str = ".construct/operator_mcp/run_operator_mcp.py";
pub fn build_operator_prompt(model_name: &str) -> String {
let provider = providers::Provider::detect(model_name);
let mut prompt =
String::with_capacity(core::OPERATOR_CORE_PROMPT.len() + provider.tool_layer().len() + 8);
prompt.push_str(core::OPERATOR_CORE_PROMPT);
prompt.push_str("\n\n");
prompt.push_str(provider.tool_layer());
prompt
}
pub const OPERATOR_PROMPT: &str = core::OPERATOR_CORE_PROMPT;
pub fn resolve_operator_mcp_path(cfg: &OperatorConfig) -> String {
let configured = cfg.mcp_path.trim();
if !configured.is_empty() {
return expand_tilde(configured);
}
let home = UserDirs::new()
.map(|u| u.home_dir().to_string_lossy().into_owned())
.unwrap_or_else(|| "~".to_string());
format!("{home}/{DEFAULT_OPERATOR_MCP_PATH_SUFFIX}")
}
pub fn operator_mcp_server_config(cfg: &OperatorConfig) -> McpServerConfig {
let script_path = resolve_operator_mcp_path(cfg);
let mut env: HashMap<String, String> = HashMap::new();
env.insert(
"CONSTRUCT_AGENT_ROOT".to_string(),
expand_tilde("~/.construct"),
);
if let Ok(token) = std::env::var("KUMIHO_SERVICE_TOKEN") {
if !token.trim().is_empty() {
env.insert("KUMIHO_AUTH_TOKEN".to_string(), token);
}
}
env.insert("KUMIHO_AUTO_CONFIGURE".to_string(), "1".to_string());
if let Ok(url) = std::env::var("CONSTRUCT_GATEWAY_URL") {
if !url.trim().is_empty() {
env.insert("CONSTRUCT_GATEWAY_URL".to_string(), url);
}
}
if let Ok(token) = std::env::var("CONSTRUCT_GATEWAY_TOKEN") {
if !token.trim().is_empty() {
env.insert("CONSTRUCT_GATEWAY_TOKEN".to_string(), token);
}
}
McpServerConfig {
name: OPERATOR_SERVER_NAME.to_string(),
transport: McpTransport::Stdio,
command: "python3".to_string(),
args: vec![script_path],
env,
url: None,
headers: HashMap::new(),
tool_timeout_secs: None,
}
}
pub fn inject_operator(mut config: Config, is_internal: bool) -> Config {
if is_internal {
return config;
}
if !config.operator.enabled {
return config;
}
config.mcp.enabled = true;
let already_registered = config
.mcp
.servers
.iter()
.any(|s| s.name == OPERATOR_SERVER_NAME);
if !already_registered {
let operator_cfg = config.operator.clone();
let mut server = operator_mcp_server_config(&operator_cfg);
if !config.kumiho.api_url.is_empty() {
server
.env
.insert("KUMIHO_API_URL".to_string(), config.kumiho.api_url.clone());
}
server.env.insert(
"KUMIHO_MEMORY_PROJECT".to_string(),
config.kumiho.memory_project.clone(),
);
server.env.insert(
"KUMIHO_HARNESS_PROJECT".to_string(),
config.kumiho.harness_project.clone(),
);
let gw_host = if config.gateway.host == "0.0.0.0" {
"127.0.0.1"
} else {
&config.gateway.host
};
let gw_port = config.gateway.port;
let gw_url = format!("http://{gw_host}:{gw_port}");
server
.env
.insert("CONSTRUCT_GATEWAY_URL".to_string(), gw_url);
if let Some(token) = config.gateway.paired_tokens.first() {
if !token.is_empty() {
server
.env
.insert("CONSTRUCT_GATEWAY_TOKEN".to_string(), token.clone());
}
}
config.mcp.servers.insert(0, server);
}
config
}
pub fn append_operator_prompt(
system_prompt: &mut String,
config: &Config,
is_internal: bool,
model_name: &str,
) {
if is_internal || !config.operator.enabled {
return;
}
if system_prompt.contains("OPERATOR MODE (Construct)")
|| system_prompt.contains("OPERATOR (Construct)")
{
return; }
let raw = build_operator_prompt(model_name);
let prompt = crate::agent::kumiho::substitute_project_names(&raw, config);
system_prompt.push_str("\n\n---\n\n");
system_prompt.push_str(&prompt);
}
const OPERATOR_CHANNEL_PROMPT: &str = "\
OPERATOR (Construct) — You have access to construct-operator MCP tools \
for multi-agent orchestration. Available tools: create_agent, \
wait_for_agent, send_agent_prompt, get_agent_activity, list_agents, \
save_agent_template, search_agent_pool, create_team, spawn_team, \
save_plan, compact_conversation, store_compaction.
Agent types: 'claude' (reasoning, review) or 'codex' (fast coding). \
Model tiering: opus for deep work, sonnet for balanced, haiku for cheap. \
Always set cwd when creating agents. Use wait_for_agent to get results.
For complex orchestration patterns, use load_skill to retrieve \
detailed instructions on demand.";
pub fn append_operator_channel_prompt(
system_prompt: &mut String,
config: &Config,
is_internal: bool,
_model_name: &str,
) {
if is_internal || !config.operator.enabled {
return;
}
if system_prompt.contains("OPERATOR MODE (Construct)")
|| system_prompt.contains("OPERATOR (Construct)")
{
return;
}
let prompt = crate::agent::kumiho::substitute_project_names(OPERATOR_CHANNEL_PROMPT, config);
system_prompt.push_str("\n\n---\n\n");
system_prompt.push_str(&prompt);
}
fn expand_tilde(path: &str) -> String {
let expanded = shellexpand::tilde(path);
let expanded_str = expanded.as_ref();
if expanded_str.starts_with('~') {
if let Some(user_dirs) = UserDirs::new() {
let home = user_dirs.home_dir();
if let Some(rest) = expanded_str.strip_prefix('~') {
return format!(
"{}{}{}",
home.display(),
if rest.starts_with('/') { "" } else { "/" },
rest.trim_start_matches('/')
);
}
}
}
expanded_str.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn inject_operator_adds_server() {
let cfg = Config::default();
assert!(
!cfg.mcp
.servers
.iter()
.any(|s| s.name == OPERATOR_SERVER_NAME)
);
let injected = inject_operator(cfg, false);
assert!(injected.mcp.enabled);
assert!(
injected
.mcp
.servers
.iter()
.any(|s| s.name == OPERATOR_SERVER_NAME)
);
}
#[test]
fn append_operator_prompt_adds_text() {
let cfg = Config::default();
let mut prompt = "## Identity\n\nYou are Construct.".to_string();
append_operator_prompt(&mut prompt, &cfg, false, "claude-sonnet-4-6");
assert!(prompt.contains("OPERATOR MODE (Construct)"));
}
#[test]
fn append_operator_prompt_is_idempotent() {
let cfg = Config::default();
let mut prompt = String::new();
append_operator_prompt(&mut prompt, &cfg, false, "claude-sonnet-4-6");
let after_first = prompt.len();
append_operator_prompt(&mut prompt, &cfg, false, "claude-sonnet-4-6");
assert_eq!(prompt.len(), after_first);
}
#[test]
fn inject_operator_skips_internal_agents() {
let cfg = Config::default();
let original_servers = cfg.mcp.servers.len();
let unchanged = inject_operator(cfg, true);
assert_eq!(unchanged.mcp.servers.len(), original_servers);
}
#[test]
fn inject_operator_is_idempotent() {
let cfg = Config::default();
let once = inject_operator(cfg, false);
let count_after_once = once
.mcp
.servers
.iter()
.filter(|s| s.name == OPERATOR_SERVER_NAME)
.count();
let twice = inject_operator(once, false);
let count_after_twice = twice
.mcp
.servers
.iter()
.filter(|s| s.name == OPERATOR_SERVER_NAME)
.count();
assert_eq!(count_after_once, count_after_twice);
}
#[test]
fn inject_operator_respects_disabled_flag() {
let mut cfg = Config::default();
cfg.operator.enabled = false;
let unchanged = inject_operator(cfg, false);
assert!(
!unchanged
.mcp
.servers
.iter()
.any(|s| s.name == OPERATOR_SERVER_NAME)
);
}
#[test]
fn build_prompt_includes_core_and_tool_layer() {
let claude_prompt = build_operator_prompt("claude-opus-4-6");
assert!(claude_prompt.contains("OPERATOR MODE (Construct)"));
assert!(claude_prompt.contains("=== TOOL USAGE ==="));
let openai_prompt = build_operator_prompt("gpt-5.4");
assert!(openai_prompt.contains("OPERATOR MODE (Construct)"));
assert!(openai_prompt.contains("=== TOOL USAGE ==="));
}
#[test]
fn different_models_get_different_tool_layers() {
let claude_prompt = build_operator_prompt("claude-opus-4-6");
let openai_prompt = build_operator_prompt("gpt-5.4");
assert_ne!(claude_prompt, openai_prompt);
assert!(claude_prompt.len() < openai_prompt.len());
}
}