diff --git a/desktop/src-tauri/src/mesh_llm/mod.rs b/desktop/src-tauri/src/mesh_llm/mod.rs index 3eb3670103..d162824dd9 100644 --- a/desktop/src-tauri/src/mesh_llm/mod.rs +++ b/desktop/src-tauri/src/mesh_llm/mod.rs @@ -4,6 +4,9 @@ mod discovery; pub use discovery::{availability_from_events, mesh_status_filter}; use discovery::{device_name_from_status, endpoint_id_from_status, enrich_status_payload_identity}; +mod preset; +pub use preset::{agent_preset, MeshAgentPreset, MeshAgentPresetRequest}; + use mesh_llm_sdk::{client, serve, EmbeddedNodeHandle, MeshDiscoveryMode}; use serde::{Deserialize, Serialize}; @@ -13,6 +16,12 @@ const MESH_STATUS_KIND: u64 = 30_621; const MESH_API_PORT_ENV: &str = "SPROUT_MESH_API_PORT"; const MESH_CONSOLE_PORT_ENV: &str = "SPROUT_MESH_CONSOLE_PORT"; const RELAY_MESH_API_KEY_PLACEHOLDER: &str = "sprout-mesh-local"; +/// ACP provider relay-mesh agents run on. Sources of truth for its command + +/// MCP live in the provider catalog (`known_acp_provider_exact`); these are +/// only the fallbacks. `sprout-agent` reads the `SPROUT_AGENT_PROVIDER` / +/// `OPENAI_COMPAT_*` env vars below — goose (the global default) does not. +const MESH_AGENT_PROVIDER_ID: &str = "sprout-agent"; +const MESH_AGENT_MCP_COMMAND: &str = "sprout-dev-mcp"; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] @@ -442,54 +451,6 @@ pub(super) fn dedupe_models(models: Vec) -> Vec, - pub mcp_command: String, - pub model: String, - pub env_vars: BTreeMap, -} - -pub fn agent_preset(request: MeshAgentPresetRequest) -> Result { - let model = request.model_id.trim(); - if model.is_empty() { - return Err("modelId is required".to_string()); - } - Ok(MeshAgentPreset { - provider_id: "relay-mesh".to_string(), - label: "Relay mesh".to_string(), - acp_command: crate::managed_agents::DEFAULT_ACP_COMMAND.to_string(), - agent_command: crate::managed_agents::DEFAULT_AGENT_COMMAND.to_string(), - agent_args: Vec::new(), - mcp_command: crate::managed_agents::DEFAULT_MCP_COMMAND.to_string(), - model: model.to_string(), - env_vars: BTreeMap::from([ - ("SPROUT_AGENT_PROVIDER".to_string(), "openai".to_string()), - ( - "OPENAI_COMPAT_BASE_URL".to_string(), - relay_mesh_api_base_url()?, - ), - ("OPENAI_COMPAT_MODEL".to_string(), model.to_string()), - ( - "OPENAI_COMPAT_API_KEY".to_string(), - RELAY_MESH_API_KEY_PLACEHOLDER.to_string(), - ), - ("OPENAI_COMPAT_API".to_string(), "chat".to_string()), - ]), - }) -} - #[cfg(test)] #[path = "mod_tests.rs"] mod mod_tests; diff --git a/desktop/src-tauri/src/mesh_llm/mod_tests.rs b/desktop/src-tauri/src/mesh_llm/mod_tests.rs index 0335d3373f..b5e13cb43a 100644 --- a/desktop/src-tauri/src/mesh_llm/mod_tests.rs +++ b/desktop/src-tauri/src/mesh_llm/mod_tests.rs @@ -32,3 +32,35 @@ fn model_ref_is_family_agnostic() { assert!(!looks_like_model_ref("Qwen3-35B")); assert!(!looks_like_model_ref("")); } + +#[test] +fn agent_preset_runs_on_sprout_agent_not_goose() { + // Regression (Tyler): the relay-mesh preset used to hand the agent the + // global default runtime (goose), which ignores the OpenAI-compat env + // vars and falls back to its own provider. Mesh agents must run on + // sprout-agent, which reads those vars. + let preset = super::agent_preset(super::MeshAgentPresetRequest { + model_id: "Qwen3-8B-Q4_K_M".to_string(), + }) + .expect("preset for a valid model id"); + + assert_eq!(preset.agent_command, "sprout-agent"); + assert_ne!(preset.agent_command, "goose"); + assert_eq!(preset.mcp_command, "sprout-dev-mcp"); + + // The env vars sprout-agent's config layer reads (crates/sprout-agent). + assert_eq!( + preset + .env_vars + .get("SPROUT_AGENT_PROVIDER") + .map(String::as_str), + Some("openai") + ); + assert_eq!( + preset + .env_vars + .get("OPENAI_COMPAT_MODEL") + .map(String::as_str), + Some("Qwen3-8B-Q4_K_M") + ); +} diff --git a/desktop/src-tauri/src/mesh_llm/preset.rs b/desktop/src-tauri/src/mesh_llm/preset.rs new file mode 100644 index 0000000000..8069cc9915 --- /dev/null +++ b/desktop/src-tauri/src/mesh_llm/preset.rs @@ -0,0 +1,69 @@ +//! Relay-mesh "Run on relay mesh" agent preset. Kept in a sibling file so +//! `mod.rs` stays under the 500-line budget; `#[path]`-included from there. +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; + +use super::{ + relay_mesh_api_base_url, MESH_AGENT_MCP_COMMAND, MESH_AGENT_PROVIDER_ID, + RELAY_MESH_API_KEY_PLACEHOLDER, +}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct MeshAgentPresetRequest { + pub model_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct MeshAgentPreset { + pub provider_id: String, + pub label: String, + pub acp_command: String, + pub agent_command: String, + pub agent_args: Vec, + pub mcp_command: String, + pub model: String, + pub env_vars: BTreeMap, +} + +pub fn agent_preset(request: MeshAgentPresetRequest) -> Result { + let model = request.model_id.trim(); + if model.is_empty() { + return Err("modelId is required".to_string()); + } + // Run on sprout-agent, not the global default (goose). Source command + + // MCP from the catalog so this can't drift from the provider definition. + let sprout_agent = crate::managed_agents::known_acp_provider_exact(MESH_AGENT_PROVIDER_ID); + let agent_command = sprout_agent + .and_then(|p| p.commands.first().copied()) + .unwrap_or(MESH_AGENT_PROVIDER_ID) + .to_string(); + let mcp_command = sprout_agent + .and_then(|p| p.mcp_command) + .unwrap_or(MESH_AGENT_MCP_COMMAND) + .to_string(); + Ok(MeshAgentPreset { + provider_id: "relay-mesh".to_string(), + label: "Relay mesh".to_string(), + acp_command: crate::managed_agents::DEFAULT_ACP_COMMAND.to_string(), + agent_command, + agent_args: Vec::new(), + mcp_command, + model: model.to_string(), + env_vars: BTreeMap::from([ + ("SPROUT_AGENT_PROVIDER".to_string(), "openai".to_string()), + ( + "OPENAI_COMPAT_BASE_URL".to_string(), + relay_mesh_api_base_url()?, + ), + ("OPENAI_COMPAT_MODEL".to_string(), model.to_string()), + ( + "OPENAI_COMPAT_API_KEY".to_string(), + RELAY_MESH_API_KEY_PLACEHOLDER.to_string(), + ), + ("OPENAI_COMPAT_API".to_string(), "chat".to_string()), + ]), + }) +}