diff --git a/crates/sprout-cli/src/commands/pack.rs b/crates/sprout-cli/src/commands/pack.rs index fe893c50c..e76cabdf8 100644 --- a/crates/sprout-cli/src/commands/pack.rs +++ b/crates/sprout-cli/src/commands/pack.rs @@ -74,11 +74,11 @@ pub fn cmd_inspect(path: &str) -> Result<(), CliError> { println!(" Display: {}", persona.display_name); println!(" Description: {}", persona.description); - if let Some(ref provider) = persona.provider { + if let Some(ref llm_provider) = persona.llm_provider { if let Some(ref model) = persona.model { - println!(" Model: {provider}:{model}"); + println!(" Model: {llm_provider}:{model}"); } else { - println!(" Provider: {provider}"); + println!(" Provider: {llm_provider}"); } } else if let Some(ref model) = persona.model { println!(" Model: {model}"); diff --git a/crates/sprout-persona/src/pack.rs b/crates/sprout-persona/src/pack.rs index 60d48a820..2fc9aacec 100644 --- a/crates/sprout-persona/src/pack.rs +++ b/crates/sprout-persona/src/pack.rs @@ -85,6 +85,8 @@ pub struct LoadedPersona { pub description: String, pub avatar: Option, pub model: Option, + /// Preferred ACP runtime ID from the persona config (e.g., 'goose', 'claude'). + pub runtime: Option, pub temperature: Option, pub max_context_tokens: Option, pub subscribe: Vec, @@ -440,6 +442,7 @@ fn parse_persona_file( description: pc.description, avatar: pc.avatar, model: resolved.model, + runtime: pc.runtime.clone(), temperature: resolved.temperature, max_context_tokens: resolved.max_context_tokens, subscribe: resolved.subscribe.unwrap_or_default(), @@ -634,6 +637,7 @@ You are Berry, a fast and direct worker. description: String::new(), avatar: None, model: None, + runtime: None, temperature: None, max_context_tokens: None, subscribe: vec![], diff --git a/crates/sprout-persona/src/persona.rs b/crates/sprout-persona/src/persona.rs index 1f17fb447..99059f4a8 100644 --- a/crates/sprout-persona/src/persona.rs +++ b/crates/sprout-persona/src/persona.rs @@ -154,6 +154,11 @@ pub struct PersonaConfig { #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, + /// Preferred ACP runtime ID (e.g., 'goose', 'claude'). Maps to PersonaRecord.runtime during + /// pack import. + #[serde(skip_serializing_if = "Option::is_none")] + pub runtime: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub temperature: Option, @@ -200,6 +205,7 @@ struct Frontmatter { #[serde(alias = "respond_to")] triggers: Option, model: Option, + runtime: Option, temperature: Option, max_context_tokens: Option, thread_replies: Option, @@ -260,6 +266,7 @@ pub fn parse_persona_md(content: &str) -> Result { subscribe: fm.subscribe, triggers: fm.triggers, model: fm.model, + runtime: fm.runtime, temperature: fm.temperature, max_context_tokens: fm.max_context_tokens, thread_replies: fm.thread_replies, diff --git a/crates/sprout-persona/src/resolve.rs b/crates/sprout-persona/src/resolve.rs index 86f296a77..15ac0fd95 100644 --- a/crates/sprout-persona/src/resolve.rs +++ b/crates/sprout-persona/src/resolve.rs @@ -35,8 +35,12 @@ pub struct ResolvedPersona { // → Config.model (plain model ID, post-split) pub model: Option, - // → PersonaRecord.provider (for desktop display / GOOSE_PROVIDER env) - pub provider: Option, + /// LLM inference provider extracted from the model string colon prefix (e.g., 'databricks' + /// from 'databricks:model-id'). Flows into harness-specific env vars (GOOSE_PROVIDER) only. + pub llm_provider: Option, + /// Preferred ACP runtime ID from the persona config (e.g., 'goose', 'claude'). Maps to + /// PersonaRecord.runtime during pack import. + pub runtime: Option, pub temperature: Option, pub max_context_tokens: Option, @@ -200,7 +204,7 @@ fn resolve_one_persona( let system_prompt = compose_prompt(&lp.prompt, pack_instructions); // Split "provider:model-id" into separate fields (V3 contract). - let (provider, model) = match lp.model.as_deref() { + let (llm_provider, model) = match lp.model.as_deref() { Some(s) if !s.trim().is_empty() => { let (prov, id) = split_model(s); ( @@ -231,7 +235,8 @@ fn resolve_one_persona( version, system_prompt, model, - provider, + llm_provider, + runtime: lp.runtime.clone(), temperature: lp.temperature, max_context_tokens: lp.max_context_tokens, subscribe: lp.subscribe.clone(), @@ -642,7 +647,7 @@ mod tests { assert_eq!(p.name, "bot"); assert_eq!(p.system_prompt, "You are Bot.\n"); assert!(p.model.is_none()); - assert!(p.provider.is_none()); + assert!(p.llm_provider.is_none()); assert!(p.triggers.mentions); // built-in default assert!(p.mcp_servers.is_empty()); assert!(p.goose_env_vars.is_empty()); @@ -685,7 +690,7 @@ mod tests { // Model split into separate fields (V3 contract) assert_eq!(p.model.as_deref(), Some("claude-sonnet-4-20250514")); - assert_eq!(p.provider.as_deref(), Some("anthropic")); + assert_eq!(p.llm_provider.as_deref(), Some("anthropic")); // Env vars projected let env_map: HashMap<&str, &str> = p @@ -740,10 +745,10 @@ mod tests { // pip overrides model assert_eq!(pip.model.as_deref(), Some("claude-4-opus-20250514")); - assert_eq!(pip.provider.as_deref(), Some("anthropic")); + assert_eq!(pip.llm_provider.as_deref(), Some("anthropic")); // lep inherits model from defaults assert_eq!(lep.model.as_deref(), Some("claude-sonnet-4-20250514")); - assert_eq!(lep.provider.as_deref(), Some("anthropic")); + assert_eq!(lep.llm_provider.as_deref(), Some("anthropic")); // pip inherits temperature from defaults assert_eq!(pip.temperature, Some(0.7)); @@ -835,7 +840,7 @@ mod tests { let pack = resolve_pack(dir).unwrap(); let p = &pack.personas[0]; assert_eq!(p.model.as_deref(), Some("gpt-4o")); - assert_eq!(p.provider.as_deref(), Some("openai")); + assert_eq!(p.llm_provider.as_deref(), Some("openai")); } #[test] @@ -859,7 +864,7 @@ mod tests { let pack = resolve_pack(dir).unwrap(); let p = &pack.personas[0]; assert_eq!(p.model.as_deref(), Some("gpt-4o")); - assert!(p.provider.is_none()); + assert!(p.llm_provider.is_none()); } // ── Test helpers ────────────────────────────────────────────────────── @@ -876,6 +881,7 @@ mod tests { description: "A test persona.".into(), avatar: None, model: model.map(str::to_owned), + runtime: None, temperature, max_context_tokens, subscribe: vec![], diff --git a/crates/sprout-persona/tests/integration.rs b/crates/sprout-persona/tests/integration.rs index 498f4905f..7e445ee95 100644 --- a/crates/sprout-persona/tests/integration.rs +++ b/crates/sprout-persona/tests/integration.rs @@ -426,12 +426,12 @@ fn resolve_full_pipeline() { assert_eq!(pip.description, "Orchestration agent"); assert_eq!(pip.version, "1.0.0"); // defaults to pack version - // Model split: "anthropic:claude-4-opus-20250514" → provider + model - assert_eq!(pip.provider.as_deref(), Some("anthropic")); + // Model split: "anthropic:claude-4-opus-20250514" → llm_provider + model + assert_eq!(pip.llm_provider.as_deref(), Some("anthropic")); assert_eq!(pip.model.as_deref(), Some("claude-4-opus-20250514")); // Lep inherits pack default model - assert_eq!(lep.provider.as_deref(), Some("anthropic")); + assert_eq!(lep.llm_provider.as_deref(), Some("anthropic")); assert_eq!(lep.model.as_deref(), Some("claude-sonnet-4-20250514")); // System prompt composed: persona body + pack instructions @@ -544,12 +544,12 @@ fn resolve_multi_persona_pack() { .unwrap(); // Alpha overrides model and temperature - assert_eq!(alpha.provider.as_deref(), Some("anthropic")); + assert_eq!(alpha.llm_provider.as_deref(), Some("anthropic")); assert_eq!(alpha.model.as_deref(), Some("claude-sonnet-4-20250514")); assert_eq!(alpha.temperature, Some(0.9)); // Beta inherits all defaults - assert_eq!(beta.provider.as_deref(), Some("openai")); + assert_eq!(beta.llm_provider.as_deref(), Some("openai")); assert_eq!(beta.model.as_deref(), Some("gpt-4o")); assert_eq!(beta.temperature, Some(0.5)); assert!(beta.thread_replies); diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs index fa26f0285..89022c616 100644 --- a/desktop/scripts/check-file-sizes.mjs +++ b/desktop/scripts/check-file-sizes.mjs @@ -33,7 +33,7 @@ const overrides = new Map([ ["src-tauri/src/managed_agents/nest.rs", 1420], // version-gated AGENTS.md + SKILL.md refresh + .agents/.claude symlink migration + ensure_skill_symlinks (all known providers) + managed section upsert + dynamic agent context + tests ["src-tauri/src/managed_agents/personas.rs", 980], // built-in persona system prompts (Solo + Kit + Scout) + merge_personas inequality checks + persona pack import/uninstall/list + uninstall safety check + retired persona migration (RETIRED_PERSONAS constant + migrate_retired_personas) ["src-tauri/src/managed_agents/teams.rs", 580], // built-in team registry (Kit & Scout) + merge_teams + validate_team_deletion + JSON export/import + tests - ["src-tauri/src/managed_agents/persona_card.rs", 970], // PNG/ZIP/MD persona card codec + pack-zip detection + nested root finder + provider/model/namePool fields + 27 unit tests + ["src-tauri/src/managed_agents/persona_card.rs", 978], // PNG/ZIP/MD persona card codec + pack-zip detection + nested root finder + runtime/model/namePool fields + backward-compat provider key read + 28 unit tests ["src/app/AppShell.tsx", 880], // message edit state + handlers + ChannelPane edit prop threading + scrollback pagination + workflows view + projects view + memory-leak safeguards + home-badge state lifted here so it consumes the same NIP-RS read-state as the sidebar (single ReadStateManager) + dock bounce wiring + mark-all-read context + channel notification callback + desktopEnabled guard + useThreadFollows wiring + isNotifiedForThread combined predicate + threadActivityItems context plumbing + mutedRootIds denylist + handleFollowThread/handleUnfollowThread combined handlers ["src/features/channels/hooks.ts", 550], // canvas query + mutation hooks + DM hide mutation ["src/features/channels/ui/ChannelManagementSheet.tsx", 800], @@ -49,17 +49,17 @@ const overrides = new Map([ ["src/features/sidebar/ui/AppSidebar.tsx", 830], // channels + forums creation forms + Pulse nav + channel sections state/dialogs + SidebarDndContext wrapper + sectionIds memo for DnD section reorder + controlled create-channel dialog for ⌘⇧N shortcut ["src/features/sidebar/ui/CustomChannelSection.tsx", 615], // ChannelGroupSection + CustomChannelSection + SectionHeaderActions + ChannelContextMenuItems + MoveToSectionSubmenu + per-section mark-all-read + DnD wrappers (SortableSectionShell, DraggableChannelRow, DroppableSectionBody, DroppableUngroupedBody) + draggable prop on ChannelGroupSection ["src/shared/api/relayClientSession.ts", 1040], // durable websocket session manager with reconnect/replay/recovery state + sendTypingIndicator + fetchChannelHistoryBefore + subscribeToChannelLive (huddle TTS) + subscribeToHuddleEvents (huddle indicator) + disconnect() for workspace switch teardown + fetchEvents/subscribeLive/publishEvent for NIP-RS read state + publishUserStatus/subscribeToUserStatusUpdates (NIP-38) + ConnectionState plumbing & stall-watchdog wiring for half-open WS detection (Warp orange-icon case) + terminal session latch (auth rejection no longer racing back to reconnecting) — emitter + watchdog + reconnect policy logic extracted to relayConnectionStateEmitter.ts / relayStallWatchdog.ts / relayReconnectPolicy.ts - ["src-tauri/src/migration.rs", 1010], // worktree shared-agent-data symlink sync (SHARED_AGENT_FILES + SHARED_AGENT_DIRS symlink-to-canonical + sibling pack migration) + mcp_command provider reconciliation + persona_pack_path reconciliation + tests + ["src-tauri/src/migration.rs", 1120], // worktree shared-agent-data symlink sync (SHARED_AGENT_FILES + SHARED_AGENT_DIRS symlink-to-canonical + sibling pack migration) + mcp_command runtime reconciliation + persona_pack_path reconciliation + provider→runtime persona migration + tests ["src-tauri/src/commands/media.rs", 730], // ffmpeg video transcode + poster frame extraction + run_ffmpeg_with_timeout (find_ffmpeg via resolve_command, is_video_file, transcode_to_mp4, extract_poster_frame, transcode_and_extract_poster) + spawn_blocking wrappers + tests ["src-tauri/src/commands/agents.rs", 881], // remote agent lifecycle routing (local + provider branches) + scope enforcement + persona pack metadata wiring + mcp_toolsets field + NIP-OA auth_tag in deploy payload ["src-tauri/src/commands/messages.rs", 515], // feed multi-query + NIP-50 search + forum thread resolution + thread ref + reactions via REQ + edit_message media_tags param (Slack-style attachment-editable edits) ["src-tauri/src/nostr_convert.rs", 1150], // 12 Nostr event→model converters (channels, profiles, members, notes, search, agents, relay members) + rank_user_search_results helper for NIP-50 user search + 33 unit tests ["src-tauri/src/managed_agents/runtime.rs", 1330], // ... + respond-to gate env (SPROUT_ACP_RESPOND_TO[_ALLOWLIST]) + per-mode env builder + tests + persona/agent env_vars spawn merge (helper + tests now in env_vars.rs) + system-wide orphan sweep (proc_listallpids/proc on macOS, /proc on Linux) + SPROUT_MANAGED_AGENT env marker check (KERN_PROCARGS2 on macOS, /proc/environ on Linux), instance-scoped by app identifier so coexisting Sprout instances don't reap each other's agents - ["src-tauri/src/managed_agents/discovery.rs", 680], // KNOWN_ACP_PROVIDERS catalog + resolve_command cache + login_shell_path + classify_provider (four-state: Available/AdapterMissing/CliMissing/NotInstalled) + discover_acp_providers with dynamic install_hint + known_acp_provider/known_acp_provider_exact + normalize_agent_args + 15 unit tests - ["src-tauri/src/managed_agents/types.rs", 745], // ManagedAgentRecord/Summary + Create/Update request structs + AcpProviderCatalogEntry + InstallRuntimeResult + RespondTo enum + validate_respond_to_allowlist + tests + persona/agent env_vars field + ["src-tauri/src/managed_agents/discovery.rs", 698], // KNOWN_ACP_RUNTIMES catalog + resolve_command cache + login_shell_path + classify_provider (four-state: Available/AdapterMissing/CliMissing/NotInstalled) + discover_acp_providers with dynamic install_hint + known_acp_provider/known_acp_provider_exact + normalize_agent_args + 15 unit tests + ["src-tauri/src/managed_agents/types.rs", 745], // ManagedAgentRecord/Summary + Create/Update request structs + AcpRuntimeCatalogEntry + InstallRuntimeResult + RespondTo enum + validate_respond_to_allowlist + tests + persona/agent env_vars field ["src-tauri/src/managed_agents/backend.rs", 700], // provider IPC, validation, discovery, binary resolution + tests + redact_secrets_with for user env values + env_secrets_from_request + redact_env_values_in (shared with model discovery) ["src/features/huddle/HuddleContext.tsx", 650], // huddle lifecycle context + joinHuddle + connectAndSetupMedia shared helper + activeSpeakers/isReconnecting state + PTT (reusable AudioContext) + TTS subscription + mic level analyser (10fps throttle) + agent pubkey refresh - ["src/features/agents/hooks.ts", 550], // agent query/mutation surface + useAvailableAcpProviders (type-narrowing filter hook) + useInstallAcpRuntimeMutation + built-in persona library activation + ["src/features/agents/hooks.ts", 550], // agent query/mutation surface + useAvailableAcpRuntimes (type-narrowing filter hook) + useInstallAcpRuntimeMutation + built-in persona library activation ["src/features/agents/ui/AgentsView.tsx", 880], // remote agent lifecycle controls + persona/team management + persona import-update dialog wiring + built-in catalog/library state orchestration ["src/features/agents/ui/UnifiedAgentsSection.tsx", 570], // unified persona-grouped agent view with collapsible groups, bulk actions, drag-drop import, empty/loading states ["src/features/agents/ui/ManagedAgentRow.tsx", 530], // EditAgentDialog integration + provider/local branching @@ -69,8 +69,8 @@ const overrides = new Map([ ["src/features/agents/ui/PersonaDialog.tsx", 515], // persona create/edit form + env vars editor + drag-drop file import + runtime provider dropdown with availability warnings ["src/features/agents/ui/CreateAgentDialog.tsx", 685], // provider selector + config form + schema-typed config coercion + required field validation + locked scopes ["src/features/channels/ui/AddChannelBotDialog.tsx", 690], // provider mode: Run on selector, trust warning, probe effect, single-agent enforcement, provider warnings display + RespondTo field + reuse guardrail - ["src/features/settings/ui/ChannelTemplatesSettingsCard.tsx", 850], // template CRUD card + TemplateFormDialog (persona/team chip selectors + provider assignments + canvas template) + TemplateTeamSelector + ProviderAssignments + ProviderRow - ["src/shared/api/types.ts", 650], // ... + AcpProviderCatalogEntry + AcpProvider (narrowed subtype) + InstallRuntimeResult + RespondToMode + respondTo/respondToAllowlist on ManagedAgent/Create/Update inputs + ["src/features/settings/ui/ChannelTemplatesSettingsCard.tsx", 850], // template CRUD card + TemplateFormDialog (persona/team chip selectors + runtime assignments + canvas template) + TemplateTeamSelector + RuntimeAssignments + RuntimeRow + ["src/shared/api/types.ts", 650], // ... + AcpRuntimeCatalogEntry + AcpRuntime (narrowed subtype) + InstallRuntimeResult + RespondToMode + respondTo/respondToAllowlist on ManagedAgent/Create/Update inputs ["src-tauri/src/events.rs", 810], // event builders + build_huddle_guidelines (kind:48106) + post_event_raw transport helper + participant p-tag on join/leave + NIP-43 relay admin builders (add/remove/change-role) + check_relay_role + DM/presence/workflow command builders + NIP-IA identity-archive builders (9035/9036) + .allow_self_tagging() guards (nostr 0.44 strips self-`p` by default; self-archive/unarchive needs it preserved) + spec vector 1 layout test ["src-tauri/src/huddle/mod.rs", 1020], // huddle state machine + Tauri commands + sync protocol doc; state/relay/pipeline extracted + emit_huddle_state_changed wiring ["src-tauri/src/huddle/models.rs", 950], // model download manager for Parakeet TDT-CTC STT + Pocket TTS with streaming downloads + SHA-256 verification + Rust-native tar extraction + version manifest + atomic swap + hot-start signaling + MODEL_LICENSE.txt sidecar (fail-closed readiness) + idempotent legacy Moonshine dir cleanup + tts_readiness_requires_license_sidecar test + Mary (VCTK p333) reference voice attribution block @@ -81,8 +81,8 @@ const overrides = new Map([ ["src-tauri/src/huddle/tts.rs", 1380], // TTS pipeline + session warmup + cancel/shutdown handling + apply_fade_out (fade-out only — leading fade removed 2026-05-18 after onset-attenuation regression measured in examples/pocket_onset_probe.rs) + FIRST_APPEND_LEAD_IN_SAMPLES + build_sentence_append_plan (pure helper enforcing the lead-in fires exactly once per utterance, not per sentence — see lead_in_pad_fires_exactly_once_per_utterance regression test) + normalize_for_playback (per-sentence peak normalization to -3 dBFS ceiling with MAX_GAIN cap) + 30 unit tests (18 interrupt + 5 fade-out + 1 first-append-lead-in + 3 build-sentence-append-plan + 6 normalize) ["src-tauri/src/relay.rs", 510], // +4 lines for NIP-OA auth tag injection in profile sync (build_profile_event) + verification test ["src-tauri/src/commands/pairing.rs", 600], // NIP-AB pairing actor: 3 Tauri commands + background WS task + NIP-42 auth + NIP-43 probe + event parsing helpers - ["src-tauri/src/lib.rs", 770], // +4 lines for PairingHandle managed state + 3 pairing command registrations + parse_message_deep_link helper extracted with 6 unit tests covering empty-param filter regression + mod migration + sync_shared_agent_data/reconcile_provider_mcp_commands/reconcile_persona_pack_paths calls on launch + SIGINT/SIGTERM/SIGHUP signal handlers for agent process cleanup - ["src/shared/api/tauri.ts", 1212], // pairing command wrappers + applyWorkspace + NIP-44 encrypt/decrypt wrappers + observer_url field + relay member API functions (list/get/add/remove/change-role) + prevent sleep + AcpProviderCatalogEntry raw types + fromRawAcpProviderCatalogEntry converter + installAcpRuntime + ["src-tauri/src/lib.rs", 770], // +4 lines for PairingHandle managed state + 3 pairing command registrations + parse_message_deep_link helper extracted with 6 unit tests covering empty-param filter regression + mod migration + sync_shared_agent_data/reconcile_provider_mcp_commands/reconcile_persona_pack_paths/migrate_persona_provider_to_runtime calls on launch + SIGINT/SIGTERM/SIGHUP signal handlers for agent process cleanup + ["src/shared/api/tauri.ts", 1212], // pairing command wrappers + applyWorkspace + NIP-44 encrypt/decrypt wrappers + observer_url field + relay member API functions (list/get/add/remove/change-role) + prevent sleep + AcpRuntimeCatalogEntry raw types + fromRawAcpRuntimeCatalogEntry converter + installAcpRuntime ]); async function walkFiles(directory) { diff --git a/desktop/src-tauri/src/commands/agent_discovery.rs b/desktop/src-tauri/src/commands/agent_discovery.rs index efc4ed308..ed98bae07 100644 --- a/desktop/src-tauri/src/commands/agent_discovery.rs +++ b/desktop/src-tauri/src/commands/agent_discovery.rs @@ -4,7 +4,7 @@ use tauri::State; use crate::{ app_state::AppState, managed_agents::{ - command_availability, AcpProviderCatalogEntry, DiscoverManagedAgentPrereqsRequest, + command_availability, AcpRuntimeCatalogEntry, DiscoverManagedAgentPrereqsRequest, InstallRuntimeResult, InstallStepResult, ManagedAgentPrereqsInfo, RelayAgentInfo, DEFAULT_ACP_COMMAND, DEFAULT_MCP_COMMAND, }, @@ -20,29 +20,29 @@ fn active_installs() -> &'static std::sync::Mutex Vec { +pub fn discover_acp_providers() -> Vec { crate::managed_agents::clear_resolve_cache(); - crate::managed_agents::discover_acp_providers() + crate::managed_agents::discover_acp_runtimes() } #[tauri::command] -pub async fn install_acp_runtime(provider_id: String) -> Result { - tokio::task::spawn_blocking(move || install_acp_runtime_blocking(&provider_id)) +pub async fn install_acp_runtime(runtime_id: String) -> Result { + tokio::task::spawn_blocking(move || install_acp_runtime_blocking(&runtime_id)) .await .map_err(|e| format!("install task panicked: {e}"))? } /// Err(_) = infrastructure failure (panic, concurrency guard). /// Ok({success: false}) = an install step failed (stderr captured in steps). -fn install_acp_runtime_blocking(provider_id: &str) -> Result { - // Prevent concurrent installs for the same provider. +fn install_acp_runtime_blocking(runtime_id: &str) -> Result { + // Prevent concurrent installs for the same runtime. { let mut set = active_installs() .lock() .map_err(|_| "install lock poisoned".to_string())?; - if !set.insert(provider_id.to_string()) { + if !set.insert(runtime_id.to_string()) { return Err(format!( - "an install is already in progress for {provider_id}" + "an install is already in progress for {runtime_id}" )); } } @@ -55,17 +55,17 @@ fn install_acp_runtime_blocking(provider_id: &str) -> Result Result p.mcp_command.unwrap_or("").to_string(), None => DEFAULT_MCP_COMMAND.to_string(), }, diff --git a/desktop/src-tauri/src/commands/personas.rs b/desktop/src-tauri/src/commands/personas.rs index b3d0d4504..99138797a 100644 --- a/desktop/src-tauri/src/commands/personas.rs +++ b/desktop/src-tauri/src/commands/personas.rs @@ -51,7 +51,7 @@ pub fn create_persona( let display_name = trim_required(&input.display_name, "Display name")?; let system_prompt = trim_required(&input.system_prompt, "System prompt")?; let avatar_url = trim_optional(input.avatar_url); - let provider = trim_optional(input.provider); + let runtime = trim_optional(input.runtime); let model = trim_optional(input.model); let now = now_iso(); @@ -72,7 +72,7 @@ pub fn create_persona( display_name, avatar_url, system_prompt, - provider, + runtime, model, name_pool, is_builtin: false, @@ -98,7 +98,7 @@ pub fn update_persona( let display_name = trim_required(&input.display_name, "Display name")?; let system_prompt = trim_required(&input.system_prompt, "System prompt")?; let avatar_url = trim_optional(input.avatar_url); - let provider = trim_optional(input.provider); + let runtime = trim_optional(input.runtime); let model = trim_optional(input.model); let _store_guard = state @@ -117,7 +117,7 @@ pub fn update_persona( persona.display_name = display_name; persona.avatar_url = avatar_url; persona.system_prompt = system_prompt; - persona.provider = provider; + persona.runtime = runtime; persona.model = model; persona.name_pool = input .name_pool @@ -335,7 +335,7 @@ pub async fn export_persona_to_json( // forked, distributed), and bundling API keys / credentials in them // would be a significant footgun. Users who import a card and need // credentials must supply them post-import via the persona dialog. - let (display_name, system_prompt, avatar_url, provider, model, name_pool) = { + let (display_name, system_prompt, avatar_url, runtime, model, name_pool) = { let _store_guard = state .managed_agents_store_lock .lock() @@ -349,7 +349,7 @@ pub async fn export_persona_to_json( persona.display_name.clone(), persona.system_prompt.clone(), persona.avatar_url.clone(), - persona.provider.clone(), + persona.runtime.clone(), persona.model.clone(), persona.name_pool.clone(), ) @@ -359,7 +359,7 @@ pub async fn export_persona_to_json( &display_name, &system_prompt, avatar_url.as_deref(), - provider.as_deref(), + runtime.as_deref(), model.as_deref(), &name_pool, )?; diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 0048a35de..b36a143a5 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -411,6 +411,7 @@ pub fn run() { migration::sync_shared_agent_data(&app_handle); migration::reconcile_provider_mcp_commands(&app_handle); migration::reconcile_persona_pack_paths(&app_handle); + migration::migrate_persona_provider_to_runtime(&app_handle); // Resolve persisted identity key (env var → file → generate+save). // This is fatal — the app should not start with an ephemeral identity diff --git a/desktop/src-tauri/src/managed_agents/discovery.rs b/desktop/src-tauri/src/managed_agents/discovery.rs index 4aa3f6c75..9b8070ab6 100644 --- a/desktop/src-tauri/src/managed_agents/discovery.rs +++ b/desktop/src-tauri/src/managed_agents/discovery.rs @@ -2,17 +2,17 @@ use std::path::{Path, PathBuf}; use std::process::Command; use crate::managed_agents::{ - AcpAvailabilityStatus, AcpProviderCatalogEntry, CommandAvailabilityInfo, + AcpAvailabilityStatus, AcpRuntimeCatalogEntry, CommandAvailabilityInfo, }; -pub(crate) struct KnownAcpProvider { +pub(crate) struct KnownAcpRuntime { pub id: &'static str, pub label: &'static str, pub commands: &'static [&'static str], pub aliases: &'static [&'static str], pub avatar_url: &'static str, /// MCP server binary to use instead of the default `sprout-mcp-server`. - /// `None` means this provider does not need a Sprout MCP server — + /// `None` means this runtime does not need a Sprout MCP server — /// no MCP tools will be registered for the agent session. pub mcp_command: Option<&'static str>, /// Whether to enable MCP hook tools (`_Stop`, `_PostCompact`) for this agent. @@ -32,8 +32,15 @@ pub(crate) struct KnownAcpProvider { /// Harness-specific skill discovery directory (e.g. `.goose/skills`). /// `Some(dir)` → Sprout creates a symlink at `//sprout-cli` /// pointing to the canonical `.agents/skills/sprout-cli`. `None` → this - /// provider reads the canonical path directly or has no skill support. + /// runtime reads the canonical path directly or has no skill support. pub skill_dir: Option<&'static str>, + pub supports_acp_model_switching: bool, + pub model_env_var: Option<&'static str>, + #[allow(dead_code)] + pub provider_env_var: Option<&'static str>, + #[allow(dead_code)] + pub provider_locked: bool, + pub default_env: &'static [(&'static str, &'static str)], } const GOOSE_AVATAR_URL: &str = "https://goose-docs.ai/img/logo_dark.png"; @@ -64,8 +71,8 @@ fn common_binary_paths() -> &'static [PathBuf] { }) } -const KNOWN_ACP_PROVIDERS: &[KnownAcpProvider] = &[ - KnownAcpProvider { +const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[ + KnownAcpRuntime { id: "goose", label: "Goose", commands: &["goose"], @@ -80,8 +87,13 @@ const KNOWN_ACP_PROVIDERS: &[KnownAcpProvider] = &[ cli_install_hint: "Install Goose via the official install script.", adapter_install_hint: "", skill_dir: Some(".goose/skills"), + supports_acp_model_switching: false, + model_env_var: Some("GOOSE_MODEL"), + provider_env_var: Some("GOOSE_PROVIDER"), + provider_locked: false, + default_env: &[("GOOSE_MODE", "auto")], }, - KnownAcpProvider { + KnownAcpRuntime { id: "claude", label: "Claude Code", commands: &["claude-agent-acp", "claude-code-acp"], @@ -96,8 +108,13 @@ const KNOWN_ACP_PROVIDERS: &[KnownAcpProvider] = &[ cli_install_hint: "Install the Claude Code CLI via the official install script.", adapter_install_hint: "Install the Claude Code ACP adapter via npm.", skill_dir: Some(".claude/skills"), + supports_acp_model_switching: false, + model_env_var: None, + provider_env_var: None, + provider_locked: true, + default_env: &[], }, - KnownAcpProvider { + KnownAcpRuntime { id: "codex", label: "Codex", commands: &["codex-acp"], @@ -112,8 +129,13 @@ const KNOWN_ACP_PROVIDERS: &[KnownAcpProvider] = &[ cli_install_hint: "Install the Codex CLI via the official install script.", adapter_install_hint: "Install the Codex ACP adapter via npm.", skill_dir: Some(".codex/skills"), + supports_acp_model_switching: false, + model_env_var: None, + provider_env_var: None, + provider_locked: true, + default_env: &[], }, - KnownAcpProvider { + KnownAcpRuntime { id: "sprout-agent", label: "Sprout Agent", commands: &["sprout-agent"], @@ -128,12 +150,17 @@ const KNOWN_ACP_PROVIDERS: &[KnownAcpProvider] = &[ cli_install_hint: "Ships with the Sprout desktop app.", adapter_install_hint: "", skill_dir: None, + supports_acp_model_switching: true, + model_env_var: None, + provider_env_var: Some("SPROUT_AGENT_PROVIDER"), + provider_locked: false, + default_env: &[], }, ]; -/// Skill discovery directories declared by known providers. +/// Skill discovery directories declared by known runtimes. pub(crate) fn known_skill_dirs() -> impl Iterator { - KNOWN_ACP_PROVIDERS.iter().filter_map(|p| p.skill_dir) + KNOWN_ACP_RUNTIMES.iter().filter_map(|p| p.skill_dir) } fn workspace_root_dir() -> PathBuf { @@ -183,21 +210,21 @@ fn normalize_command_identity(command: &str) -> String { lower } -pub(crate) fn known_acp_provider(command: &str) -> Option<&'static KnownAcpProvider> { +pub(crate) fn known_acp_runtime(command: &str) -> Option<&'static KnownAcpRuntime> { let normalized = normalize_command_identity(command); - KNOWN_ACP_PROVIDERS.iter().find(|provider| { - normalized == provider.id - || provider + KNOWN_ACP_RUNTIMES.iter().find(|runtime| { + normalized == runtime.id + || runtime .commands .iter() .any(|command| normalized == normalize_command_identity(command)) - || provider.aliases.iter().any(|alias| normalized == *alias) + || runtime.aliases.iter().any(|alias| normalized == *alias) }) } -pub(crate) fn known_acp_provider_exact(id: &str) -> Option<&'static KnownAcpProvider> { - KNOWN_ACP_PROVIDERS.iter().find(|p| p.id == id) +pub(crate) fn known_acp_runtime_exact(id: &str) -> Option<&'static KnownAcpRuntime> { + KNOWN_ACP_RUNTIMES.iter().find(|p| p.id == id) } fn default_agent_args(command: &str) -> Option> { @@ -413,7 +440,7 @@ pub fn missing_command_message(command: &str, role: &str) -> String { ) } -fn classify_provider( +fn classify_runtime( adapter_result: Option<(&str, PathBuf)>, underlying_cli: Option<&str>, underlying_cli_found: bool, @@ -439,27 +466,24 @@ fn classify_provider( } } -pub fn discover_acp_providers() -> Vec { - KNOWN_ACP_PROVIDERS +pub fn discover_acp_runtimes() -> Vec { + KNOWN_ACP_RUNTIMES .iter() - .map(|provider| { + .map(|runtime| { // Try to find the ACP adapter binary. - let adapter_result = provider + let adapter_result = runtime .commands .iter() .find_map(|command| find_command(command).map(|path| (*command, path))); - let underlying_cli_found = provider + let underlying_cli_found = runtime .underlying_cli .map(|cli| find_command(cli).is_some()) .unwrap_or(false); - let (availability, command, binary_path) = classify_provider( - adapter_result, - provider.underlying_cli, - underlying_cli_found, - ); + let (availability, command, binary_path) = + classify_runtime(adapter_result, runtime.underlying_cli, underlying_cli_found); - let underlying_cli_path = provider + let underlying_cli_path = runtime .underlying_cli .and_then(find_command) .map(|p| p.display().to_string()); @@ -469,11 +493,11 @@ pub fn discover_acp_providers() -> Vec { .map(|cmd| normalize_agent_args(cmd, Vec::new())) .unwrap_or_default(); - let can_auto_install = !provider.cli_install_commands.is_empty() - || !provider.adapter_install_commands.is_empty(); + let can_auto_install = !runtime.cli_install_commands.is_empty() + || !runtime.adapter_install_commands.is_empty(); - let cli_hint = provider.cli_install_hint; - let adapter_hint = provider.adapter_install_hint; + let cli_hint = runtime.cli_install_hint; + let adapter_hint = runtime.adapter_install_hint; let install_hint = match availability { AcpAvailabilityStatus::Available => cli_hint.to_string(), AcpAvailabilityStatus::CliMissing => cli_hint.to_string(), @@ -489,17 +513,17 @@ pub fn discover_acp_providers() -> Vec { } }; - AcpProviderCatalogEntry { - id: provider.id.to_string(), - label: provider.label.to_string(), - avatar_url: provider.avatar_url.to_string(), + AcpRuntimeCatalogEntry { + id: runtime.id.to_string(), + label: runtime.label.to_string(), + avatar_url: runtime.avatar_url.to_string(), availability, command, binary_path, default_args, - mcp_command: provider.mcp_command.map(str::to_string), + mcp_command: runtime.mcp_command.map(str::to_string), install_hint, - install_instructions_url: provider.install_instructions_url.to_string(), + install_instructions_url: runtime.install_instructions_url.to_string(), can_auto_install, underlying_cli_path, } @@ -508,8 +532,8 @@ pub fn discover_acp_providers() -> Vec { } pub fn managed_agent_avatar_url(command: &str) -> Option { - let provider = known_acp_provider(command)?; - Some(provider.avatar_url.to_string()) + let runtime = known_acp_runtime(command)?; + Some(runtime.avatar_url.to_string()) } #[cfg(test)] @@ -517,7 +541,7 @@ mod tests { use std::path::PathBuf; use super::{ - classify_provider, find_via_login_shell, managed_agent_avatar_url, normalize_agent_args, + classify_runtime, find_via_login_shell, managed_agent_avatar_url, normalize_agent_args, CLAUDE_CODE_AVATAR_URL, CODEX_AVATAR_URL, GOOSE_AVATAR_URL, SPROUT_AGENT_AVATAR_URL, }; use crate::managed_agents::AcpAvailabilityStatus; @@ -614,7 +638,7 @@ mod tests { #[test] fn classifies_available_when_adapter_found() { - let (status, cmd, path) = classify_provider( + let (status, cmd, path) = classify_runtime( Some(("goose", PathBuf::from("/usr/local/bin/goose"))), None, false, @@ -626,7 +650,7 @@ mod tests { #[test] fn classifies_adapter_missing_when_cli_present() { - let (status, cmd, path) = classify_provider(None, Some("claude"), true); + let (status, cmd, path) = classify_runtime(None, Some("claude"), true); assert_eq!(status, AcpAvailabilityStatus::AdapterMissing); assert!(cmd.is_none()); assert!(path.is_none()); @@ -634,7 +658,7 @@ mod tests { #[test] fn classifies_not_installed_when_nothing_found() { - let (status, cmd, path) = classify_provider(None, Some("claude"), false); + let (status, cmd, path) = classify_runtime(None, Some("claude"), false); assert_eq!(status, AcpAvailabilityStatus::NotInstalled); assert!(cmd.is_none()); assert!(path.is_none()); @@ -642,7 +666,7 @@ mod tests { #[test] fn classifies_not_installed_when_no_underlying_cli() { - let (status, cmd, path) = classify_provider(None, None, false); + let (status, cmd, path) = classify_runtime(None, None, false); assert_eq!(status, AcpAvailabilityStatus::NotInstalled); assert!(cmd.is_none()); assert!(path.is_none()); @@ -650,7 +674,7 @@ mod tests { #[test] fn classifies_cli_missing_when_adapter_found_but_cli_absent() { - let (status, cmd, path) = classify_provider( + let (status, cmd, path) = classify_runtime( Some(("codex-acp", PathBuf::from("/opt/homebrew/bin/codex-acp"))), Some("codex"), false, diff --git a/desktop/src-tauri/src/managed_agents/nest.rs b/desktop/src-tauri/src/managed_agents/nest.rs index cbf6b85a3..7ac81515b 100644 --- a/desktop/src-tauri/src/managed_agents/nest.rs +++ b/desktop/src-tauri/src/managed_agents/nest.rs @@ -941,7 +941,7 @@ mod tests { display_name: display_name.to_string(), avatar_url: None, system_prompt: String::new(), - provider: None, + runtime: None, model: None, name_pool: vec![], is_builtin: false, diff --git a/desktop/src-tauri/src/managed_agents/persona_card.rs b/desktop/src-tauri/src/managed_agents/persona_card.rs index c4a9ec109..c2539da28 100644 --- a/desktop/src-tauri/src/managed_agents/persona_card.rs +++ b/desktop/src-tauri/src/managed_agents/persona_card.rs @@ -13,7 +13,7 @@ pub struct ParsedPersonaPreview { pub display_name: String, pub system_prompt: String, pub avatar_data_url: Option, - pub provider: Option, + pub runtime: Option, pub model: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub name_pool: Vec, @@ -80,7 +80,7 @@ pub fn parse_png_persona(png_bytes: &[u8]) -> Result, - provider: Option, + runtime: Option, model: Option, name_pool: Vec, } @@ -135,8 +135,10 @@ fn extract_sprout_fields(v: &Value) -> Result { .map(|s| s.trim()) .filter(|s| !s.is_empty()) .map(|s| s.to_string()); - let provider = v - .get("provider") + // Read "runtime" with backward-compat fallback to legacy "provider" key. + let runtime = v + .get("runtime") + .or_else(|| v.get("provider")) .and_then(|v| v.as_str()) .map(|s| s.trim()) .filter(|s| !s.is_empty()) @@ -162,7 +164,7 @@ fn extract_sprout_fields(v: &Value) -> Result { display_name: name, system_prompt: prompt, avatar_url, - provider, + runtime, model, name_pool, }) @@ -206,7 +208,7 @@ fn parse_chara_payload(b64: &str) -> Result { display_name: name, system_prompt: prompt, avatar_url: None, - provider: None, + runtime: None, model: None, name_pool: Vec::new(), }) @@ -224,7 +226,7 @@ pub fn parse_json_persona(json_bytes: &[u8]) -> Result, - provider: Option<&str>, + runtime: Option<&str>, model: Option<&str>, name_pool: &[String], ) -> Result, String> { @@ -246,8 +248,8 @@ pub fn encode_persona_json( if let Some(url) = avatar_url { map.insert("avatarUrl".to_string(), serde_json::json!(url)); } - if let Some(p) = provider { - map.insert("provider".to_string(), serde_json::json!(p)); + if let Some(r) = runtime { + map.insert("runtime".to_string(), serde_json::json!(r)); } if let Some(m) = model { map.insert("model".to_string(), serde_json::json!(m)); @@ -271,19 +273,19 @@ pub fn parse_md_persona(md_bytes: &[u8]) -> Result .map_err(|e| format!("Failed to parse .persona.md: {e}"))?; // Split "provider:model" into separate fields for the preview. - let (provider, model) = match config.model.as_deref() { + let model = match config.model.as_deref() { Some(s) if !s.is_empty() => { - let (prov, id) = sprout_persona::persona::split_model(s); - (prov.map(str::to_owned), Some(id.to_owned())) + let (_prov, id) = sprout_persona::persona::split_model(s); + Some(id.to_owned()) } - _ => (None, None), + _ => None, }; Ok(ParsedPersonaPreview { display_name: config.display_name, system_prompt: config.prompt, avatar_data_url: None, // .persona.md avatars are paths, not data URIs - provider, + runtime: config.runtime, model, name_pool: Vec::new(), source_file: String::new(), @@ -383,7 +385,7 @@ pub fn parse_zip_pack(zip_bytes: &[u8]) -> Result, - provider: Option<&'static str>, + runtime: Option<&'static str>, } const SOLO_AVATAR: &str = "data:image/png;base64,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"; @@ -148,7 +148,7 @@ const BUILT_IN_PERSONAS: &[BuiltInPersona] = &[ "Birch", "Dune", "Fern", "Moss", "Reed", "Slate", "Ash", "Brook", "Glen", "Stone", ], model: None, - provider: None, + runtime: None, }, BuiltInPersona { id: "builtin:kit", @@ -297,7 +297,7 @@ Don't present work that doesn't meet this bar. No emojis. Your name is Kit. You are friendly and helpful. You are understated, but have a sense of humor."#, model: None, - provider: None, + runtime: None, }, BuiltInPersona { id: "builtin:scout", @@ -457,7 +457,7 @@ If you're invoked outside a Sprout team channel (or by an agent that isn't Kit), Your name is Scout. You are friendly and helpful. You are understated, but have a sense of humor."#, model: None, - provider: None, + runtime: None, }, ]; @@ -500,7 +500,7 @@ fn built_in_persona_records(now: &str) -> Vec { display_name: persona.display_name.to_string(), avatar_url: persona.avatar_url.map(|s| s.to_string()), system_prompt: persona.system_prompt.to_string(), - provider: persona.provider.map(|s| s.to_string()), + runtime: persona.runtime.map(|s| s.to_string()), model: persona.model.map(|s| s.to_string()), name_pool: persona.name_pool.iter().map(|s| s.to_string()).collect(), is_builtin: true, @@ -550,15 +550,15 @@ fn merge_personas(mut stored: Vec, now: &str) -> (Vec PersonaRecord { display_name: display_name.to_string(), avatar_url: Some("https://example.com/avatar.png".to_string()), system_prompt: "Custom prompt".to_string(), - provider: None, + runtime: None, model: None, name_pool: Vec::new(), is_builtin: false, diff --git a/desktop/src-tauri/src/managed_agents/runtime.rs b/desktop/src-tauri/src/managed_agents/runtime.rs index 9acd34803..cc108d11f 100644 --- a/desktop/src-tauri/src/managed_agents/runtime.rs +++ b/desktop/src-tauri/src/managed_agents/runtime.rs @@ -4,7 +4,7 @@ use tauri::AppHandle; use crate::{ managed_agents::{ - append_log_marker, known_acp_provider, login_shell_path, managed_agent_log_path, + append_log_marker, known_acp_runtime, login_shell_path, managed_agent_log_path, missing_command_message, normalize_agent_args, open_log_file, resolve_command, ManagedAgentProcess, ManagedAgentRecord, ManagedAgentSummary, }, @@ -859,7 +859,8 @@ pub fn spawn_agent_child( } // Enable MCP hook tools (_Stop, _PostCompact) for agents that need them. // Uses "*" because build_mcp_servers() hard-codes the server name to "sprout-mcp". - if known_acp_provider(&record.agent_command).is_some_and(|p| p.mcp_hooks) { + let runtime_meta = known_acp_runtime(&record.agent_command); + if runtime_meta.is_some_and(|r| r.mcp_hooks) { command.env("MCP_HOOK_SERVERS", "*"); } // Only emit SPROUT_ACP_IDLE_TIMEOUT when the user has explicitly set an @@ -879,10 +880,13 @@ pub fn spawn_agent_child( command.env("SPROUT_ACP_AGENTS", record.parallelism.to_string()); command.env("SPROUT_ACP_MULTIPLE_EVENT_HANDLING", "owner-interrupt"); command.env("SPROUT_ACP_DEDUP", "queue"); - command.env( - "GOOSE_MODE", - std::env::var("GOOSE_MODE").unwrap_or_else(|_| "auto".to_string()), - ); + if let Some(meta) = runtime_meta { + for (key, value) in meta.default_env { + if std::env::var(key).is_err() { + command.env(key, value); + } + } + } if let (Some(pack_path), Some(persona_name)) = (&record.persona_pack_path, &record.persona_name_in_pack) { @@ -925,6 +929,13 @@ pub fn spawn_agent_child( } else { command.env_remove("SPROUT_ACP_MODEL"); } + if let Some(meta) = runtime_meta { + if !meta.supports_acp_model_switching { + if let (Some(env_key), Some(model)) = (meta.model_env_var, &effective_model) { + command.env(env_key, model); + } + } + } if let Some(toolsets) = &record.mcp_toolsets { command.env("SPROUT_TOOLSETS", toolsets); } else { @@ -1147,7 +1158,7 @@ pub fn stop_managed_agent_process( #[cfg(test)] mod tests { - use crate::managed_agents::known_acp_provider; + use crate::managed_agents::known_acp_runtime; #[test] fn marker_entry_is_namespaced_by_instance_id() { @@ -1167,26 +1178,26 @@ mod tests { #[test] fn sprout_agent_has_mcp_hooks() { - let p = known_acp_provider("sprout-agent").expect("should resolve"); + let p = known_acp_runtime("sprout-agent").expect("should resolve"); assert!(p.mcp_hooks); assert_eq!(p.mcp_command, Some("sprout-dev-mcp")); } #[test] fn sprout_agent_resolved_via_path() { - assert!(known_acp_provider("/usr/local/bin/sprout-agent").is_some_and(|p| p.mcp_hooks)); + assert!(known_acp_runtime("/usr/local/bin/sprout-agent").is_some_and(|p| p.mcp_hooks)); } #[test] fn goose_has_no_mcp_hooks() { - let p = known_acp_provider("goose").expect("should resolve"); + let p = known_acp_runtime("goose").expect("should resolve"); assert!(!p.mcp_hooks); assert_eq!(p.mcp_command, None); } #[test] fn unknown_command_returns_none() { - assert!(known_acp_provider("custom-agent").is_none()); + assert!(known_acp_runtime("custom-agent").is_none()); } // ── build_respond_to_env tests ─────────────────────────────────────── diff --git a/desktop/src-tauri/src/managed_agents/teams.rs b/desktop/src-tauri/src/managed_agents/teams.rs index 2860678f5..1833eeb4d 100644 --- a/desktop/src-tauri/src/managed_agents/teams.rs +++ b/desktop/src-tauri/src/managed_agents/teams.rs @@ -329,7 +329,7 @@ mod tests { display_name: name.to_string(), avatar_url: None, system_prompt: prompt.to_string(), - provider: None, + runtime: None, model: None, name_pool: Vec::new(), is_builtin: false, diff --git a/desktop/src-tauri/src/managed_agents/types.rs b/desktop/src-tauri/src/managed_agents/types.rs index 2252979d0..15707371d 100644 --- a/desktop/src-tauri/src/managed_agents/types.rs +++ b/desktop/src-tauri/src/managed_agents/types.rs @@ -19,12 +19,13 @@ pub struct PersonaRecord { pub display_name: String, pub avatar_url: Option, pub system_prompt: String, - /// Preferred ACP provider ID (e.g. "goose", "claude", "codex"). - /// When deploying an agent from this persona, this provider is pre-selected. + /// Preferred ACP runtime ID (e.g., 'goose', 'claude', 'codex'). Determines which agent binary + /// Sprout spawns. When deploying from this persona, this runtime is pre-selected in the UI. #[serde(default, skip_serializing_if = "Option::is_none")] - pub provider: Option, - /// Preferred model ID (e.g. "gpt-4o", "claude-sonnet-4-20250514"). - /// Passed to the agent at creation time when deploying from this persona. + pub runtime: Option, + /// Opaque, harness-specific model identifier string. Format depends on the runtime and its LLM + /// provider (e.g., 'goose-claude-4-6-opus' for Databricks, 'claude-opus-4-7' for Anthropic + /// direct). Sprout stores and passes through without interpretation. #[serde(default, skip_serializing_if = "Option::is_none")] pub model: Option, /// Pool of short, thematic names for bot instances created from this persona. @@ -44,9 +45,8 @@ pub struct PersonaRecord { /// Validated: `[a-zA-Z0-9_-]+`, max 64 chars (safe for env vars and paths). #[serde(default, skip_serializing_if = "Option::is_none")] pub source_pack_persona_slug: Option, - /// Environment variables injected when launching agents created from this - /// persona. Layered as: desktop parent env < persona `env_vars` < - /// individual agent `env_vars` (last wins on collision). + /// Harness-level configuration passed to the agent subprocess as environment variables. + /// Opaque to Sprout — keys and values are runtime-specific. /// /// Stored as a BTreeMap for deterministic on-disk ordering. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] @@ -236,7 +236,7 @@ pub struct CreatePersonaRequest { pub avatar_url: Option, pub system_prompt: String, #[serde(default)] - pub provider: Option, + pub runtime: Option, #[serde(default)] pub model: Option, #[serde(default)] @@ -254,7 +254,7 @@ pub struct UpdatePersonaRequest { pub avatar_url: Option, pub system_prompt: String, #[serde(default)] - pub provider: Option, + pub runtime: Option, #[serde(default)] pub model: Option, #[serde(default)] @@ -285,7 +285,7 @@ pub enum AcpAvailabilityStatus { } #[derive(Debug, Clone, Serialize)] -pub struct AcpProviderCatalogEntry { +pub struct AcpRuntimeCatalogEntry { pub id: String, pub label: String, pub avatar_url: String, @@ -551,7 +551,7 @@ mod tests { assert!(record.is_active); assert!(!record.is_builtin); - assert_eq!(record.provider, None); + assert_eq!(record.runtime, None); assert_eq!(record.model, None); assert!(record.name_pool.is_empty()); } diff --git a/desktop/src-tauri/src/migration.rs b/desktop/src-tauri/src/migration.rs index 9fd13b30d..975c27d00 100644 --- a/desktop/src-tauri/src/migration.rs +++ b/desktop/src-tauri/src/migration.rs @@ -270,17 +270,17 @@ fn reconcile_mcp_commands_in_file(path: &Path) { Some(cmd) => cmd.to_string(), None => return false, }; - let Some(provider) = crate::managed_agents::known_acp_provider(&agent_command) else { + let Some(runtime) = crate::managed_agents::known_acp_runtime(&agent_command) else { return false; }; - let expected = provider.mcp_command.unwrap_or(""); + let expected = runtime.mcp_command.unwrap_or(""); let current = obj .get("mcp_command") .and_then(|v| v.as_str()) .unwrap_or(""); if current != expected { eprintln!( - "sprout-desktop: provider-reconcile: {:?} ({:?}): mcp_command {:?} → {:?}", + "sprout-desktop: runtime-reconcile: {:?} ({:?}): mcp_command {:?} → {:?}", obj.get("name").and_then(|v| v.as_str()).unwrap_or("?"), agent_command, current, @@ -298,7 +298,7 @@ fn reconcile_mcp_commands_in_file(path: &Path) { } /// Reconcile `mcp_command` values in managed-agents.json against the -/// discovery table. Known providers get their canonical mcp_command; +/// discovery table. Known runtimes get their canonical mcp_command; /// unknown/custom agents are left untouched. pub fn reconcile_provider_mcp_commands(app: &tauri::AppHandle) { let Ok(dir) = app.path().app_data_dir() else { @@ -370,6 +370,30 @@ pub fn reconcile_persona_pack_paths(app: &tauri::AppHandle) { reconcile_pack_paths_in_file(&path, &canonical_dir); } +fn rename_provider_to_runtime_in_personas(path: &Path) { + patch_json_records(path, |obj| { + if obj.contains_key("runtime") { + return false; + } + if let Some(value) = obj.remove("provider") { + obj.insert("runtime".to_string(), value); + true + } else { + false + } + }); +} + +pub fn migrate_persona_provider_to_runtime(app: &tauri::AppHandle) { + let Ok(dir) = app.path().app_data_dir() else { + return; + }; + let path = dir.join("agents/personas.json"); + if !path.exists() { + return; + } + rename_provider_to_runtime_in_personas(&path); +} #[cfg(test)] mod tests { use super::*; @@ -1001,4 +1025,93 @@ mod tests { assert_eq!(after_first, after_second); } + + fn write_personas_json(dir: &Path, records: &serde_json::Value) { + std::fs::create_dir_all(dir.join("agents")).unwrap(); + std::fs::write( + dir.join("agents/personas.json"), + serde_json::to_vec_pretty(records).unwrap(), + ) + .unwrap(); + } + + fn read_personas_json(dir: &Path) -> Vec { + let content = std::fs::read_to_string(dir.join("agents/personas.json")).unwrap(); + serde_json::from_str(&content).unwrap() + } + + #[test] + fn rename_provider_to_runtime_migrates_field() { + let dir = tempfile::tempdir().unwrap(); + write_personas_json( + dir.path(), + &serde_json::json!([{ + "id": "persona-1", + "displayName": "Alice", + "provider": "goose" + }]), + ); + rename_provider_to_runtime_in_personas(&dir.path().join("agents/personas.json")); + let records = read_personas_json(dir.path()); + assert_eq!(records[0]["runtime"], "goose"); + assert!(records[0].get("provider").is_none()); + } + + #[test] + fn rename_provider_to_runtime_is_idempotent() { + let dir = tempfile::tempdir().unwrap(); + write_personas_json( + dir.path(), + &serde_json::json!([{ + "id": "persona-1", + "displayName": "Alice", + "runtime": "goose" + }]), + ); + let before = std::fs::read_to_string(dir.path().join("agents/personas.json")).unwrap(); + rename_provider_to_runtime_in_personas(&dir.path().join("agents/personas.json")); + let after = std::fs::read_to_string(dir.path().join("agents/personas.json")).unwrap(); + assert_eq!( + before, after, + "file should not be rewritten when already migrated" + ); + } + + #[test] + fn rename_provider_to_runtime_skips_record_without_either_key() { + let dir = tempfile::tempdir().unwrap(); + write_personas_json( + dir.path(), + &serde_json::json!([{ + "id": "persona-1", + "displayName": "Alice" + }]), + ); + let before = std::fs::read_to_string(dir.path().join("agents/personas.json")).unwrap(); + rename_provider_to_runtime_in_personas(&dir.path().join("agents/personas.json")); + let after = std::fs::read_to_string(dir.path().join("agents/personas.json")).unwrap(); + assert_eq!( + before, after, + "file should not be rewritten when no provider key exists" + ); + } + + #[test] + fn rename_provider_to_runtime_preserves_existing_runtime_over_provider() { + let dir = tempfile::tempdir().unwrap(); + write_personas_json( + dir.path(), + &serde_json::json!([{ + "id": "persona-1", + "displayName": "Alice", + "provider": "old-value", + "runtime": "correct-value" + }]), + ); + rename_provider_to_runtime_in_personas(&dir.path().join("agents/personas.json")); + let records = read_personas_json(dir.path()); + assert_eq!(records[0]["runtime"], "correct-value"); + // provider key should still be there since the closure returns false when runtime exists + assert_eq!(records[0]["provider"], "old-value"); + } } diff --git a/desktop/src-tauri/src/templates/storage.rs b/desktop/src-tauri/src/templates/storage.rs index 8a2151e6e..ba39f7721 100644 --- a/desktop/src-tauri/src/templates/storage.rs +++ b/desktop/src-tauri/src/templates/storage.rs @@ -174,14 +174,14 @@ mod tests { agents: TemplateAgentRoster { personas: vec![TemplateAgentEntry { persona_id: "builtin:kit".to_string(), - provider: Some("claude".to_string()), + runtime: Some("claude".to_string()), model: Some("opus".to_string()), role: Some("bot".to_string()), backend: Some(TemplateBackend::Local), }], teams: vec![TemplateTeamEntry { team_id: "team-1".to_string(), - provider: None, + runtime: None, model: None, backend: Some(TemplateBackend::Provider { id: "provider-1".to_string(), @@ -205,10 +205,7 @@ mod tests { assert_eq!(parsed.agents.personas.len(), 1); assert_eq!(parsed.agents.teams.len(), 1); assert_eq!(parsed.agents.personas[0].persona_id, "builtin:kit"); - assert_eq!( - parsed.agents.personas[0].provider.as_deref(), - Some("claude") - ); + assert_eq!(parsed.agents.personas[0].runtime.as_deref(), Some("claude")); assert_eq!(parsed.agents.teams[0].team_id, "team-1"); assert!(!parsed.is_builtin); } @@ -227,6 +224,19 @@ mod tests { assert!(parsed.agents.teams.is_empty()); } + #[test] + fn deserialization_backward_compat_provider_alias() { + use crate::templates::{TemplateAgentEntry, TemplateTeamEntry}; + + let agent_json = r#"{"personaId":"builtin:kit","provider":"goose"}"#; + let agent: TemplateAgentEntry = serde_json::from_str(agent_json).unwrap(); + assert_eq!(agent.runtime.as_deref(), Some("goose")); + + let team_json = r#"{"teamId":"team-1","provider":"claude"}"#; + let team: TemplateTeamEntry = serde_json::from_str(team_json).unwrap(); + assert_eq!(team.runtime.as_deref(), Some("claude")); + } + // ----------------------------------------------------------------------- // validate_channel_template_deletion // ----------------------------------------------------------------------- diff --git a/desktop/src-tauri/src/templates/types.rs b/desktop/src-tauri/src/templates/types.rs index c03a36920..3c62bb0aa 100644 --- a/desktop/src-tauri/src/templates/types.rs +++ b/desktop/src-tauri/src/templates/types.rs @@ -37,8 +37,8 @@ pub struct TemplateAgentRoster { #[serde(rename_all = "camelCase")] pub struct TemplateAgentEntry { pub persona_id: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub provider: Option, + #[serde(default, skip_serializing_if = "Option::is_none", alias = "provider")] + pub runtime: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub model: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -51,8 +51,8 @@ pub struct TemplateAgentEntry { #[serde(rename_all = "camelCase")] pub struct TemplateTeamEntry { pub team_id: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub provider: Option, + #[serde(default, skip_serializing_if = "Option::is_none", alias = "provider")] + pub runtime: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub model: Option, #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/desktop/src/features/agents/channelAgents.ts b/desktop/src/features/agents/channelAgents.ts index c9c78047e..56368dc6b 100644 --- a/desktop/src/features/agents/channelAgents.ts +++ b/desktop/src/features/agents/channelAgents.ts @@ -17,15 +17,15 @@ import { uploadMediaBytes, } from "@/shared/api/tauri"; import type { - AcpProvider, + AcpRuntime, ChannelRole, ManagedAgent, ManagedAgentBackend, RespondToMode, } from "@/shared/api/types"; -type ChannelAgentProvider = Pick< - AcpProvider, +type ChannelAgentRuntime = Pick< + AcpRuntime, "id" | "label" | "command" | "defaultArgs" | "mcpCommand" >; @@ -43,7 +43,7 @@ export type AttachManagedAgentToChannelResult = { }; export type EnsureChannelAgentPresetInput = { - provider: ChannelAgentProvider; + runtime: ChannelAgentRuntime; role?: Exclude; ensureRunning?: boolean; }; @@ -51,11 +51,11 @@ export type EnsureChannelAgentPresetInput = { export type EnsureChannelAgentPresetResult = AttachManagedAgentToChannelResult & { created: boolean; - providerId: string; + runtimeId: string; }; export type CreateChannelManagedAgentInput = { - provider: ChannelAgentProvider; + runtime: ChannelAgentRuntime; name: string; systemPrompt?: string; avatarUrl?: string; @@ -76,7 +76,7 @@ export type CreateChannelManagedAgentInput = { export type CreateChannelManagedAgentResult = AttachManagedAgentToChannelResult & { created: boolean; - providerId: string; + runtimeId: string; }; export type CreateChannelManagedAgentBatchFailure = { @@ -147,25 +147,25 @@ export async function attachManagedAgentToChannel( } satisfies AttachManagedAgentToChannelResult; } -function buildChannelAgentName(providerId: string, providerLabel: string) { - const normalizedProviderId = providerId.trim().toLowerCase(); - if (normalizedProviderId.length > 0) { - return normalizedProviderId; +function buildChannelAgentName(runtimeId: string, runtimeLabel: string) { + const normalizedRuntimeId = runtimeId.trim().toLowerCase(); + if (normalizedRuntimeId.length > 0) { + return normalizedRuntimeId; } - return providerLabel.trim().toLowerCase() || "agent"; + return runtimeLabel.trim().toLowerCase() || "agent"; } function pickPreferredChannelPresetAgent( agents: ManagedAgent[], memberPubkeys: ReadonlySet, - providerCommand: string, + runtimeCommand: string, expectedName: string, ) { const inChannelAgent = pickPreferredManagedAgent( agents.filter( (agent) => - commandsMatch(agent.agentCommand, providerCommand) && + commandsMatch(agent.agentCommand, runtimeCommand) && memberPubkeys.has(normalizePubkey(agent.pubkey)), ), ); @@ -176,7 +176,7 @@ function pickPreferredChannelPresetAgent( return pickPreferredManagedAgent( agents.filter( (agent) => - commandsMatch(agent.agentCommand, providerCommand) && + commandsMatch(agent.agentCommand, runtimeCommand) && agent.name.trim().toLowerCase() === expectedName.trim().toLowerCase(), ), ); @@ -194,13 +194,13 @@ export async function ensureChannelAgentPresetInChannel( ); const managedAgents = await listManagedAgents(); const expectedName = buildChannelAgentName( - input.provider.id, - input.provider.label, + input.runtime.id, + input.runtime.label, ); const existingAgent = pickPreferredChannelPresetAgent( managedAgents, memberPubkeys, - input.provider.command, + input.runtime.command, expectedName, ); @@ -213,16 +213,16 @@ export async function ensureChannelAgentPresetInChannel( return { ...attached, created: false, - providerId: input.provider.id, + runtimeId: input.runtime.id, }; } const created = await createManagedAgent({ name: expectedName, acpCommand: "sprout-acp", - agentCommand: input.provider.command, - agentArgs: input.provider.defaultArgs, - mcpCommand: input.provider.mcpCommand ?? "sprout-mcp-server", + agentCommand: input.runtime.command, + agentArgs: input.runtime.defaultArgs, + mcpCommand: input.runtime.mcpCommand ?? "sprout-mcp-server", spawnAfterCreate: false, }); const attached = await attachManagedAgentToChannel(channelId, { @@ -234,7 +234,7 @@ export async function ensureChannelAgentPresetInChannel( return { ...attached, created: true, - providerId: input.provider.id, + runtimeId: input.runtime.id, }; } @@ -293,7 +293,7 @@ export async function createChannelManagedAgent( return { ...attached, created: false, - providerId: input.provider.id, + runtimeId: input.runtime.id, }; } } @@ -309,7 +309,7 @@ export async function createChannelManagedAgent( ) { const reusable = findReusableGenericAgent( context.managedAgents, - input.provider.command, + input.runtime.command, context.channelMemberPubkeys, ); if (reusable) { @@ -336,7 +336,7 @@ export async function createChannelManagedAgent( return { ...attached, created: false, - providerId: input.provider.id, + runtimeId: input.runtime.id, }; } } @@ -362,9 +362,9 @@ export async function createChannelManagedAgent( const created = await createManagedAgent({ name: trimmedName, acpCommand: "sprout-acp", - agentCommand: input.provider.command, - agentArgs: input.provider.defaultArgs, - mcpCommand: input.provider.mcpCommand ?? "sprout-mcp-server", + agentCommand: input.runtime.command, + agentArgs: input.runtime.defaultArgs, + mcpCommand: input.runtime.mcpCommand ?? "sprout-mcp-server", personaId: input.personaId ?? undefined, systemPrompt: input.systemPrompt?.trim() || undefined, avatarUrl: resolvedAvatarUrl, @@ -390,7 +390,7 @@ export async function createChannelManagedAgent( return { ...attached, created: true, - providerId: input.provider.id, + runtimeId: input.runtime.id, }; } diff --git a/desktop/src/features/agents/hooks.ts b/desktop/src/features/agents/hooks.ts index d7fe52237..f69e02f9b 100644 --- a/desktop/src/features/agents/hooks.ts +++ b/desktop/src/features/agents/hooks.ts @@ -11,7 +11,7 @@ import { channelsQueryKey } from "@/features/channels/hooks"; import { createManagedAgent, deleteManagedAgent, - discoverAcpProviders, + discoverAcpRuntimes, discoverBackendProviders, discoverManagedAgentPrereqs, getManagedAgentLog, @@ -38,7 +38,7 @@ import { updateTeam, } from "@/shared/api/tauriTeams"; import type { - AcpProvider, + AcpRuntime, AgentPersona, AgentTeam, CreateManagedAgentInput, @@ -74,7 +74,7 @@ export const relayAgentsQueryKey = ["relay-agents"] as const; export const managedAgentsQueryKey = ["managed-agents"] as const; export const personasQueryKey = ["personas"] as const; export const teamsQueryKey = ["teams"] as const; -export const acpProvidersQueryKey = ["acp-providers"] as const; +export const acpRuntimesQueryKey = ["acp-runtimes"] as const; export const managedAgentPrereqsQueryKey = ["managed-agent-prereqs"] as const; export const backendProvidersQueryKey = ["backend-providers"] as const; @@ -100,20 +100,20 @@ async function invalidateAgentQueries( ]); } -export function useAcpProvidersQuery() { +export function useAcpRuntimesQuery() { return useQuery({ - queryKey: acpProvidersQueryKey, - queryFn: discoverAcpProviders, + queryKey: acpRuntimesQueryKey, + queryFn: discoverAcpRuntimes, staleTime: 60_000, }); } -export function useAvailableAcpProviders() { - const query = useAcpProvidersQuery(); +export function useAvailableAcpRuntimes() { + const query = useAcpRuntimesQuery(); const available = React.useMemo( () => (query.data ?? []).filter( - (p): p is AcpProvider => p.availability === "available", + (p): p is AcpRuntime => p.availability === "available", ), [query.data], ); @@ -123,9 +123,9 @@ export function useAvailableAcpProviders() { export function useInstallAcpRuntimeMutation() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: (providerId: string) => installAcpRuntime(providerId), + mutationFn: (runtimeId: string) => installAcpRuntime(runtimeId), onSettled: () => { - void queryClient.invalidateQueries({ queryKey: acpProvidersQueryKey }); + void queryClient.invalidateQueries({ queryKey: acpRuntimesQueryKey }); }, }); } @@ -456,7 +456,7 @@ export function useEnsureGooseInChannelMutation(channelId: string | null) { } const attached = await ensureChannelAgentPresetInChannel(channelId, { - provider: { + runtime: { id: "goose", label: "Goose", command: "goose", diff --git a/desktop/src/features/agents/lib/catalog.test.mjs b/desktop/src/features/agents/lib/catalog.test.mjs index 608e10577..5c6833847 100644 --- a/desktop/src/features/agents/lib/catalog.test.mjs +++ b/desktop/src/features/agents/lib/catalog.test.mjs @@ -15,7 +15,7 @@ function createPersona(id, displayName, overrides = {}) { displayName, avatarUrl: overrides.avatarUrl ?? null, systemPrompt: overrides.systemPrompt ?? `${displayName} prompt`, - provider: overrides.provider ?? null, + runtime: overrides.runtime ?? null, model: overrides.model ?? null, isBuiltIn: overrides.isBuiltIn ?? false, isActive: overrides.isActive ?? true, diff --git a/desktop/src/features/agents/lib/resolvePersonaProvider.ts b/desktop/src/features/agents/lib/resolvePersonaProvider.ts deleted file mode 100644 index 346265eb5..000000000 --- a/desktop/src/features/agents/lib/resolvePersonaProvider.ts +++ /dev/null @@ -1,91 +0,0 @@ -import type { AcpProvider } from "@/shared/api/types"; - -/** - * Result of resolving a persona's preferred provider against the set of - * currently-available ACP providers. - * - * `provider` is the provider that should be used for deployment. - * `warnings` contains user-visible messages when the resolved provider - * differs from what the persona requested (e.g. the configured runtime - * was uninstalled) or when no provider is available at all. - */ -export type ResolvePersonaProviderResult = { - provider: AcpProvider | null; - warnings: string[]; -}; - -/** - * Resolve which ACP provider to use when deploying an agent from a persona. - * - * Resolution order: - * 1. If the persona has no `providerId` → use `defaultProvider`, no warnings. - * 2. If the persona's `providerId` matches an available provider → use it. - * 3. If the persona's `providerId` is set but not found in `providers` → - * fall back to `defaultProvider` and emit a warning. - * 4. If there is no `defaultProvider` either → return `null` with an error - * warning so the UI can block deployment. - */ -export function resolvePersonaProvider( - personaProviderId: string | undefined | null, - providers: readonly AcpProvider[], - defaultProvider: AcpProvider | null, -): ResolvePersonaProviderResult { - // Case 1: Persona has no provider preference — use the default. - if (!personaProviderId) { - return { - provider: defaultProvider, - warnings: defaultProvider - ? [] - : [ - "No agent runtimes are available. Install a runtime (e.g. Goose) to deploy agents.", - ], - }; - } - - // Case 2: Persona's preferred provider is available. - const matched = providers.find((p) => p.id === personaProviderId); - if (matched) { - return { provider: matched, warnings: [] }; - } - - // Case 3 & 4: Persona's provider is not available — fall back. - if (defaultProvider) { - return { - provider: defaultProvider, - warnings: [ - `Persona is configured for runtime "${personaProviderId}" but it is not available. Using ${defaultProvider.label} instead.`, - ], - }; - } - - return { - provider: null, - warnings: [ - `Persona is configured for runtime "${personaProviderId}" but it is not available, and no other runtimes were found.`, - ], - }; -} - -/** - * Collect provider-resolution warnings for a list of personas. - * - * Used by deploy dialogs to surface inline alerts when one or more - * personas reference a runtime that isn't currently available. - */ -export function collectProviderWarnings( - personas: readonly { provider: string | null }[], - providers: readonly AcpProvider[], - fallbackProvider: AcpProvider | null, -): string[] { - if (!fallbackProvider) return []; - const warnings: string[] = []; - for (const persona of personas) { - const { warnings: w } = resolvePersonaProvider( - persona.provider, - providers, - fallbackProvider, - ); - warnings.push(...w); - } - return warnings; -} diff --git a/desktop/src/features/agents/lib/resolvePersonaRuntime.test.mjs b/desktop/src/features/agents/lib/resolvePersonaRuntime.test.mjs new file mode 100644 index 000000000..a280028fc --- /dev/null +++ b/desktop/src/features/agents/lib/resolvePersonaRuntime.test.mjs @@ -0,0 +1,207 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + collectRuntimeWarnings, + resolvePersonaRuntime, +} from "./resolvePersonaRuntime.ts"; + +// --------------------------------------------------------------------------- +// Fixtures +// --------------------------------------------------------------------------- + +function makeRuntime(id, label = `${id} label`) { + return { id, label, command: id, avatarUrl: "" }; +} + +const goose = makeRuntime("goose", "Goose"); +const claude = makeRuntime("claude", "Claude"); +const runtimes = [goose, claude]; + +// --------------------------------------------------------------------------- +// resolvePersonaRuntime — Case 1: no personaRuntimeId +// --------------------------------------------------------------------------- + +test("resolvePersonaRuntime — no personaRuntimeId returns defaultRuntime with no warnings", () => { + const result = resolvePersonaRuntime(null, runtimes, goose); + assert.deepEqual(result, { + runtime: goose, + warnings: [], + isOverridden: false, + }); +}); + +test("resolvePersonaRuntime — undefined personaRuntimeId also returns defaultRuntime", () => { + const result = resolvePersonaRuntime(undefined, runtimes, goose); + assert.deepEqual(result, { + runtime: goose, + warnings: [], + isOverridden: false, + }); +}); + +test("resolvePersonaRuntime — no personaRuntimeId and no defaultRuntime returns null with warning", () => { + const result = resolvePersonaRuntime(null, runtimes, null); + assert.equal(result.runtime, null); + assert.equal(result.warnings.length, 1); + assert.match(result.warnings[0], /No agent runtimes are available/); + assert.equal(result.isOverridden, false); +}); + +// --------------------------------------------------------------------------- +// resolvePersonaRuntime — Case 2: matching runtime found +// --------------------------------------------------------------------------- + +test("resolvePersonaRuntime — matching runtime found returns matched runtime, no warnings", () => { + const result = resolvePersonaRuntime("goose", runtimes, claude); + assert.deepEqual(result, { + runtime: goose, + warnings: [], + isOverridden: false, + }); +}); + +test("resolvePersonaRuntime — override=true with same runtime as default returns default, no warnings", () => { + const result = resolvePersonaRuntime("goose", runtimes, goose, true); + assert.deepEqual(result, { + runtime: goose, + warnings: [], + isOverridden: false, + }); +}); + +test("resolvePersonaRuntime — override=true with different default emits override warning and returns default", () => { + const result = resolvePersonaRuntime("goose", runtimes, claude, true); + assert.equal(result.runtime, claude); + assert.equal(result.warnings.length, 1); + assert.match(result.warnings[0], /Runtime override/); + assert.match(result.warnings[0], /Claude/); + assert.match(result.warnings[0], /Goose/); + assert.equal(result.isOverridden, true); +}); + +test("resolvePersonaRuntime — override=false returns matched runtime, ignores override flag", () => { + const result = resolvePersonaRuntime("goose", runtimes, claude, false); + assert.deepEqual(result, { + runtime: goose, + warnings: [], + isOverridden: false, + }); +}); + +test("resolvePersonaRuntime — override=true but no defaultRuntime returns matched runtime, no warnings", () => { + const result = resolvePersonaRuntime("goose", runtimes, null, true); + assert.deepEqual(result, { + runtime: goose, + warnings: [], + isOverridden: false, + }); +}); + +// --------------------------------------------------------------------------- +// resolvePersonaRuntime — Case 3: personaRuntimeId not in runtimes, has default +// --------------------------------------------------------------------------- + +test("resolvePersonaRuntime — unrecognised runtimeId falls back to defaultRuntime with warning", () => { + const result = resolvePersonaRuntime("unknown-rt", runtimes, goose); + assert.equal(result.runtime, goose); + assert.equal(result.warnings.length, 1); + assert.match(result.warnings[0], /unknown-rt/); + assert.match(result.warnings[0], /Goose/); + assert.match(result.warnings[0], /not available/); + assert.equal(result.isOverridden, true); +}); + +// --------------------------------------------------------------------------- +// resolvePersonaRuntime — Case 4: personaRuntimeId not in runtimes, no default +// --------------------------------------------------------------------------- + +test("resolvePersonaRuntime — unrecognised runtimeId and no defaultRuntime returns null with error warning", () => { + const result = resolvePersonaRuntime("unknown-rt", [], null); + assert.equal(result.runtime, null); + assert.equal(result.warnings.length, 1); + assert.match(result.warnings[0], /unknown-rt/); + assert.match(result.warnings[0], /no other runtimes were found/); + assert.equal(result.isOverridden, false); +}); + +// --------------------------------------------------------------------------- +// resolvePersonaRuntime — isOverridden field +// --------------------------------------------------------------------------- + +test("resolvePersonaRuntime — isOverridden is true when override redirects to different runtime", () => { + const result = resolvePersonaRuntime("goose", runtimes, claude, true); + assert.equal(result.isOverridden, true); +}); + +test("resolvePersonaRuntime — isOverridden is false when no override active", () => { + const result = resolvePersonaRuntime("goose", runtimes, claude); + assert.equal(result.isOverridden, false); +}); + +test("resolvePersonaRuntime — isOverridden is true when persona's runtime is unavailable and falls back", () => { + const result = resolvePersonaRuntime("unknown-rt", runtimes, goose); + assert.equal(result.isOverridden, true); +}); + +test("resolvePersonaRuntime — isOverridden is false when override selects same runtime as persona", () => { + const result = resolvePersonaRuntime("goose", runtimes, goose, true); + assert.equal(result.isOverridden, false); +}); + +// --------------------------------------------------------------------------- +// collectRuntimeWarnings +// --------------------------------------------------------------------------- + +test("collectRuntimeWarnings — no fallbackRuntime returns empty array regardless of personas", () => { + const personas = [{ runtime: "goose" }, { runtime: "unknown-rt" }]; + const warnings = collectRuntimeWarnings(personas, runtimes, null); + assert.deepEqual(warnings, []); +}); + +test("collectRuntimeWarnings — all personas match their runtimes returns empty array", () => { + const personas = [{ runtime: "goose" }, { runtime: "claude" }]; + const warnings = collectRuntimeWarnings(personas, runtimes, goose); + assert.deepEqual(warnings, []); +}); + +test("collectRuntimeWarnings — persona with no runtime preference produces no warning", () => { + const personas = [{ runtime: null }]; + const warnings = collectRuntimeWarnings(personas, runtimes, goose); + assert.deepEqual(warnings, []); +}); + +test("collectRuntimeWarnings — mixed personas: matching ones are silent, non-matching emit warnings", () => { + const personas = [{ runtime: "goose" }, { runtime: "unknown-rt" }]; + const warnings = collectRuntimeWarnings(personas, runtimes, goose); + assert.equal(warnings.length, 1); + assert.match(warnings[0], /unknown-rt/); +}); + +test("collectRuntimeWarnings — override mode collects one warning per persona whose runtime differs from default", () => { + const personas = [{ runtime: "goose" }, { runtime: "goose" }]; + const warnings = collectRuntimeWarnings(personas, runtimes, claude, true); + assert.equal(warnings.length, 2); + for (const w of warnings) { + assert.match(w, /Runtime override/); + } +}); + +test("collectRuntimeWarnings — override with one matching, one mismatching persona emits one warning", () => { + const personas = [{ runtime: "claude" }, { runtime: "goose" }]; + const warnings = collectRuntimeWarnings(personas, runtimes, claude, true); + assert.equal(warnings.length, 1); + assert.match(warnings[0], /Runtime override/); + assert.match(warnings[0], /Goose/); +}); + +test("collectRuntimeWarnings — override=false behaves identically to no override flag", () => { + const personas = [{ runtime: "goose" }, { runtime: "claude" }]; + const withoutFlag = collectRuntimeWarnings(personas, runtimes, goose); + const withFalse = collectRuntimeWarnings(personas, runtimes, goose, false); + assert.deepEqual(withoutFlag, withFalse); +}); + +test("collectRuntimeWarnings — empty personas array always returns empty", () => { + assert.deepEqual(collectRuntimeWarnings([], runtimes, goose, true), []); +}); diff --git a/desktop/src/features/agents/lib/resolvePersonaRuntime.ts b/desktop/src/features/agents/lib/resolvePersonaRuntime.ts new file mode 100644 index 000000000..be0e93689 --- /dev/null +++ b/desktop/src/features/agents/lib/resolvePersonaRuntime.ts @@ -0,0 +1,118 @@ +import type { AcpRuntime } from "@/shared/api/types"; + +/** + * Result of resolving a persona's preferred runtime against the set of + * currently-available ACP runtimes. + * + * `runtime` is the runtime that should be used for deployment. + * `warnings` contains user-visible messages when the resolved runtime + * differs from what the persona requested (e.g. the configured runtime + * was uninstalled) or when no runtime is available at all. + * `isOverridden` is true when the resolved runtime differs from what the + * persona originally requested (either via explicit override or fallback). + */ +export type ResolvePersonaRuntimeResult = { + runtime: AcpRuntime | null; + warnings: string[]; + isOverridden: boolean; +}; + +/** + * Resolve which ACP runtime to use when deploying an agent from a persona. + * + * Resolution order: + * 1. If the persona has no `runtimeId` → use `defaultRuntime`, no warnings. + * 2. If the persona's `runtimeId` matches an available runtime → use it, + * unless `forceOverride` is true and `defaultRuntime` is set, in which case + * `defaultRuntime` is used instead (with an info warning if they differ). + * 3. If the persona's `runtimeId` is set but not found in `runtimes` → + * fall back to `defaultRuntime` and emit a warning. + * 4. If there is no `defaultRuntime` either → return `null` with an error + * warning so the UI can block deployment. + */ +export function resolvePersonaRuntime( + personaRuntimeId: string | undefined | null, + runtimes: readonly AcpRuntime[], + defaultRuntime: AcpRuntime | null, + forceOverride?: boolean, +): ResolvePersonaRuntimeResult { + // Case 1: Persona has no runtime preference — use the default. + if (!personaRuntimeId) { + return { + runtime: defaultRuntime, + warnings: defaultRuntime + ? [] + : [ + "No agent runtimes are available. Install a runtime (e.g. Goose) to deploy agents.", + ], + isOverridden: false, + }; + } + + // Case 2: Persona's preferred runtime is available. + const matched = runtimes.find((p) => p.id === personaRuntimeId); + if (matched) { + if (forceOverride && defaultRuntime && matched.id !== defaultRuntime.id) { + return { + runtime: defaultRuntime, + warnings: [ + `Runtime override: using ${defaultRuntime.label} instead of ${matched.label}.`, + ], + isOverridden: true, + }; + } + return { + runtime: forceOverride && defaultRuntime ? defaultRuntime : matched, + warnings: [], + isOverridden: false, + }; + } + + // Case 3 & 4: Persona's runtime is not available — fall back. + if (defaultRuntime) { + return { + runtime: defaultRuntime, + warnings: [ + `Persona is configured for runtime "${personaRuntimeId}" but it is not available. Using ${defaultRuntime.label} instead.`, + ], + isOverridden: true, + }; + } + + return { + runtime: null, + warnings: [ + `Persona is configured for runtime "${personaRuntimeId}" but it is not available, and no other runtimes were found.`, + ], + isOverridden: false, + }; +} + +/** + * Collect runtime-resolution warnings for a list of personas. + * + * Used by deploy dialogs to surface inline alerts when one or more + * personas reference a runtime that isn't currently available. + */ +export function collectRuntimeWarnings( + personas: readonly { runtime: string | null }[], + runtimes: readonly AcpRuntime[], + fallbackRuntime: AcpRuntime | null, + forceOverride?: boolean, +): string[] { + // When no fallback runtime exists, the caller's UI is responsible for + // showing the global "no runtimes found" state. Per-persona warnings + // would be redundant noise alongside that. + if (!fallbackRuntime) return []; + const warnings: string[] = []; + for (const persona of personas) { + const { warnings: w } = resolvePersonaRuntime( + persona.runtime, + runtimes, + fallbackRuntime, + forceOverride, + ); + warnings.push(...w); + } + return warnings; +} diff --git a/desktop/src/features/agents/lib/teamPersonas.test.mjs b/desktop/src/features/agents/lib/teamPersonas.test.mjs index 159ba8910..34d2246e9 100644 --- a/desktop/src/features/agents/lib/teamPersonas.test.mjs +++ b/desktop/src/features/agents/lib/teamPersonas.test.mjs @@ -13,7 +13,7 @@ function createPersona(id, displayName) { displayName, avatarUrl: null, systemPrompt: `${displayName} prompt`, - provider: null, + runtime: null, model: null, isBuiltIn: false, isActive: true, diff --git a/desktop/src/features/agents/lib/useBotRecents.test.mjs b/desktop/src/features/agents/lib/useBotRecents.test.mjs index 1679dc190..e704139d9 100644 --- a/desktop/src/features/agents/lib/useBotRecents.test.mjs +++ b/desktop/src/features/agents/lib/useBotRecents.test.mjs @@ -9,7 +9,7 @@ function createPersona(id, displayName) { displayName, avatarUrl: null, systemPrompt: `${displayName} prompt`, - provider: null, + runtime: null, model: null, isBuiltIn: true, isActive: true, diff --git a/desktop/src/features/agents/lib/useLastRuntime.ts b/desktop/src/features/agents/lib/useLastRuntime.ts new file mode 100644 index 000000000..565a7eb7a --- /dev/null +++ b/desktop/src/features/agents/lib/useLastRuntime.ts @@ -0,0 +1,34 @@ +import * as React from "react"; + +const STORAGE_KEY = "sprout:last-runtime"; +const LEGACY_STORAGE_KEY = "sprout:last-runtime-provider"; + +export function useLastRuntime(): { + lastRuntimeId: string | null; + setLastRuntime: (id: string) => void; +} { + const [lastRuntimeId, setLastRuntimeId] = React.useState( + () => { + try { + return ( + localStorage.getItem(STORAGE_KEY) ?? + localStorage.getItem(LEGACY_STORAGE_KEY) + ); + } catch { + return null; + } + }, + ); + + const setLastRuntime = React.useCallback((id: string) => { + setLastRuntimeId(id); + try { + localStorage.setItem(STORAGE_KEY, id); + localStorage.removeItem(LEGACY_STORAGE_KEY); + } catch { + // localStorage full — ignore + } + }, []); + + return { lastRuntimeId, setLastRuntime }; +} diff --git a/desktop/src/features/agents/lib/useLastRuntimeProvider.ts b/desktop/src/features/agents/lib/useLastRuntimeProvider.ts deleted file mode 100644 index 085e1a505..000000000 --- a/desktop/src/features/agents/lib/useLastRuntimeProvider.ts +++ /dev/null @@ -1,29 +0,0 @@ -import * as React from "react"; - -const STORAGE_KEY = "sprout:last-runtime-provider"; - -export function useLastRuntimeProvider(): { - lastProviderId: string | null; - setLastProvider: (id: string) => void; -} { - const [lastProviderId, setLastProviderId] = React.useState( - () => { - try { - return localStorage.getItem(STORAGE_KEY); - } catch { - return null; - } - }, - ); - - const setLastProvider = React.useCallback((id: string) => { - setLastProviderId(id); - try { - localStorage.setItem(STORAGE_KEY, id); - } catch { - // localStorage full — ignore - } - }, []); - - return { lastProviderId, setLastProvider }; -} diff --git a/desktop/src/features/agents/ui/AddTeamToChannelDialog.tsx b/desktop/src/features/agents/ui/AddTeamToChannelDialog.tsx index d9d033538..5a9895e74 100644 --- a/desktop/src/features/agents/ui/AddTeamToChannelDialog.tsx +++ b/desktop/src/features/agents/ui/AddTeamToChannelDialog.tsx @@ -2,7 +2,7 @@ import { AlertTriangle } from "lucide-react"; import * as React from "react"; import { - useAvailableAcpProviders, + useAvailableAcpRuntimes, useCreateChannelManagedAgentsMutation, } from "@/features/agents/hooks"; import type { CreateChannelManagedAgentsResult } from "@/features/agents/channelAgents"; @@ -11,9 +11,9 @@ import { resolveTeamPersonas, } from "@/features/agents/lib/teamPersonas"; import { - collectProviderWarnings, - resolvePersonaProvider, -} from "@/features/agents/lib/resolvePersonaProvider"; + collectRuntimeWarnings, + resolvePersonaRuntime, +} from "@/features/agents/lib/resolvePersonaRuntime"; import { useChannelsQuery } from "@/features/channels/hooks"; import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar"; import type { @@ -50,7 +50,7 @@ export function AddTeamToChannelDialog({ onDeployed, }: AddTeamToChannelDialogProps) { const channelsQuery = useChannelsQuery(); - const providersQuery = useAvailableAcpProviders(); + const providersQuery = useAvailableAcpRuntimes(); const [channelId, setChannelId] = React.useState(""); const [role, setRole] = React.useState>("bot"); const deployMutation = useCreateChannelManagedAgentsMutation( @@ -76,11 +76,11 @@ export function AddTeamToChannelDialog({ const resolved = teamPersonaResolution.resolvedPersonas; const missingPersonaCount = teamPersonaResolution.missingPersonaCount; - // Surface warnings when a persona's preferred provider is unavailable. - // This dialog has no provider selector, so the fallback is always + // Surface warnings when a persona's preferred runtime is unavailable. + // This dialog has no runtime selector, so the fallback is always // `defaultProvider` (the first available runtime). - const providerWarnings = React.useMemo( - () => collectProviderWarnings(resolved, providers, defaultProvider), + const runtimeWarnings = React.useMemo( + () => collectRuntimeWarnings(resolved, providers, defaultProvider), [resolved, providers, defaultProvider], ); @@ -115,24 +115,24 @@ export function AddTeamToChannelDialog({ } try { - // Resolve each persona's preferred provider. This dialog has no - // provider selector, so the fallback is `defaultProvider` (first + // Resolve each persona's preferred runtime. This dialog has no + // runtime selector, so the fallback is `defaultProvider` (first // available runtime). Warnings are computed separately via the - // `providerWarnings` memo and rendered as inline alerts above. + // `runtimeWarnings` memo and rendered as inline alerts above. const inputs = resolved.map((persona) => { - const { provider: personaProvider } = resolvePersonaProvider( - persona.provider, + const { runtime: personaRuntime } = resolvePersonaRuntime( + persona.runtime, providers, defaultProvider, ); - const providerToUse = personaProvider ?? defaultProvider; + const runtimeToUse = personaRuntime ?? defaultProvider; return { - provider: { - id: providerToUse.id, - label: providerToUse.label, - command: providerToUse.command, - defaultArgs: providerToUse.defaultArgs, - mcpCommand: providerToUse.mcpCommand, + runtime: { + id: runtimeToUse.id, + label: runtimeToUse.label, + command: runtimeToUse.command, + defaultArgs: runtimeToUse.defaultArgs, + mcpCommand: runtimeToUse.mcpCommand, }, name: persona.displayName, systemPrompt: persona.systemPrompt, @@ -246,13 +246,13 @@ export function AddTeamToChannelDialog({ {!defaultProvider && !providersQuery.isLoading ? (

- No ACP providers found. Make sure an agent runtime (e.g. Goose) + No ACP runtimes found. Make sure an agent runtime (e.g. Goose) is installed.

) : null} - {providerWarnings.length > 0 - ? providerWarnings.map((warning) => ( + {runtimeWarnings.length > 0 + ? runtimeWarnings.map((warning) => (
void; }) { const createMutation = useCreateManagedAgentMutation(); - const providersQuery = useAvailableAcpProviders(); - const allProvidersQuery = useAcpProvidersQuery(); + const providersQuery = useAvailableAcpRuntimes(); + const allProvidersQuery = useAcpRuntimesQuery(); const backendProvidersQuery = useBackendProvidersQuery(); - const { lastProviderId, setLastProvider } = useLastRuntimeProvider(); + const { lastRuntimeId, setLastRuntime } = useLastRuntime(); const [acpCommand, setAcpCommand] = React.useState("sprout-acp"); const [agentCommand, setAgentCommand] = React.useState("goose"); const [agentArgs, setAgentArgs] = React.useState("acp"); @@ -67,7 +67,7 @@ export function CreateAgentDialog({ const [parallelism, setParallelism] = React.useState("24"); const [systemPrompt, setSystemPrompt] = React.useState(""); const [envVars, setEnvVars] = React.useState({}); - const [selectedProviderId, setSelectedProviderId] = + const [selectedRuntimeId, setSelectedRuntimeId] = React.useState("custom"); const [hasSyncedProviderSelection, setHasSyncedProviderSelection] = React.useState(false); @@ -86,17 +86,16 @@ export function CreateAgentDialog({ React.useState(null); const [probeError, setProbeError] = React.useState(null); - const providers = providersQuery.data ?? []; + const runtimes = providersQuery.data ?? []; const allProviders = allProvidersQuery.data ?? []; const unavailableCount = allProviders.filter( (p) => p.availability !== "available", ).length; const backendProviders = backendProvidersQuery.data ?? []; const prereqs = prereqsQuery.data ?? null; - const selectedProvider = React.useMemo( - () => - providers.find((provider) => provider.id === selectedProviderId) ?? null, - [providers, selectedProviderId], + const selectedRuntime = React.useMemo( + () => runtimes.find((runtime) => runtime.id === selectedRuntimeId) ?? null, + [runtimes, selectedRuntimeId], ); const selectedBackendProvider = React.useMemo( () => backendProviders.find((p) => p.id === runOn) ?? null, @@ -115,28 +114,28 @@ export function CreateAgentDialog({ return; } - // Prefer last-used provider from localStorage - const remembered = lastProviderId - ? providers.find((provider) => provider.id === lastProviderId) + // Prefer last-used runtime from localStorage + const remembered = lastRuntimeId + ? runtimes.find((runtime) => runtime.id === lastRuntimeId) : null; if (remembered) { - setSelectedProviderId(remembered.id); + setSelectedRuntimeId(remembered.id); setAgentCommand(remembered.command); setAgentArgs(remembered.defaultArgs.join(",")); setMcpCommand(remembered.mcpCommand ?? "sprout-mcp-server"); } else { const matchingProvider = - providers.find((provider) => provider.command === agentCommand) ?? null; + runtimes.find((runtime) => runtime.command === agentCommand) ?? null; if (matchingProvider) { - setSelectedProviderId(matchingProvider.id); + setSelectedRuntimeId(matchingProvider.id); } } setHasSyncedProviderSelection(true); }, [ agentCommand, hasSyncedProviderSelection, - lastProviderId, - providers, + lastRuntimeId, + runtimes, providersQuery.isLoading, ]); @@ -221,7 +220,7 @@ export function CreateAgentDialog({ setParallelism("24"); setSystemPrompt(""); setEnvVars({}); - setSelectedProviderId("custom"); + setSelectedRuntimeId("custom"); setHasSyncedProviderSelection(false); setShowAdvanced(false); setRunOn("local"); @@ -242,21 +241,21 @@ export function CreateAgentDialog({ } function handleProviderChange(nextProviderId: string) { - setSelectedProviderId(nextProviderId); + setSelectedRuntimeId(nextProviderId); if (nextProviderId === "custom") { setShowAdvanced(true); return; } - const provider = providers.find( + const provider = runtimes.find( (candidate) => candidate.id === nextProviderId, ); if (!provider) { return; } - setLastProvider(nextProviderId); + setLastRuntime(nextProviderId); setAgentCommand(provider.command); setAgentArgs(provider.defaultArgs.join(",")); setMcpCommand(provider.mcpCommand ?? "sprout-mcp-server"); @@ -443,12 +442,12 @@ export function CreateAgentDialog({ {/* Local mode: show the ACP runtime selector */} {!isProviderMode ? ( - ) : null} @@ -520,7 +519,7 @@ export function CreateAgentDialog({ onTurnTimeoutChange={setTurnTimeoutSeconds} parallelism={parallelism} relayUrl={relayUrl} - selectedProviderId={selectedProviderId} + selectedRuntimeId={selectedRuntimeId} systemPrompt={systemPrompt} turnTimeoutSeconds={turnTimeoutSeconds} /> diff --git a/desktop/src/features/agents/ui/CreateAgentDialogSections.tsx b/desktop/src/features/agents/ui/CreateAgentDialogSections.tsx index c917f2018..3b9cb6078 100644 --- a/desktop/src/features/agents/ui/CreateAgentDialogSections.tsx +++ b/desktop/src/features/agents/ui/CreateAgentDialogSections.tsx @@ -1,4 +1,4 @@ -import type { AcpProvider, ManagedAgentPrereqs } from "@/shared/api/types"; +import type { AcpRuntime, ManagedAgentPrereqs } from "@/shared/api/types"; import { cn } from "@/shared/lib/cn"; import { Input } from "@/shared/ui/input"; import { Textarea } from "@/shared/ui/textarea"; @@ -36,50 +36,50 @@ export function CreateAgentBasicsFields({ ); } -export function CreateAgentRuntimeProviderField({ - providers, - providersLoading, - selectedProvider, - selectedProviderId, +export function CreateAgentRuntimeField({ + runtimes, + runtimesLoading, + selectedRuntime, + selectedRuntimeId, unavailableCount, - onProviderChange, + onRuntimeChange, }: { - providers: AcpProvider[]; - providersLoading: boolean; - selectedProvider: AcpProvider | null; - selectedProviderId: string; + runtimes: AcpRuntime[]; + runtimesLoading: boolean; + selectedRuntime: AcpRuntime | null; + selectedRuntimeId: string; unavailableCount: number; - onProviderChange: (value: string) => void; + onRuntimeChange: (value: string) => void; }) { return (
-
- {selectedProviderId === "custom" ? ( + {selectedRuntimeId === "custom" ? (

- Preferred provider + Preferred runtime

- {persona.provider ?? "Use app default"} + {persona.runtime ?? "Use app default"}

diff --git a/desktop/src/features/agents/ui/PersonaDialog.tsx b/desktop/src/features/agents/ui/PersonaDialog.tsx index e9768db12..f6fd3d756 100644 --- a/desktop/src/features/agents/ui/PersonaDialog.tsx +++ b/desktop/src/features/agents/ui/PersonaDialog.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { RefreshCw, Upload } from "lucide-react"; import type { - AcpProviderCatalogEntry, + AcpRuntimeCatalogEntry, CreatePersonaInput, UpdatePersonaInput, } from "@/shared/api/types"; @@ -35,8 +35,8 @@ type PersonaDialogProps = { error: Error | null; isPending: boolean; isImportPending?: boolean; - providers: AcpProviderCatalogEntry[]; - providersLoading?: boolean; + runtimes: AcpRuntimeCatalogEntry[]; + runtimesLoading?: boolean; onOpenChange: (open: boolean) => void; onSubmit: (input: CreatePersonaInput | UpdatePersonaInput) => Promise; onImportUpdateFile?: ( @@ -55,8 +55,8 @@ export function PersonaDialog({ error, isPending, isImportPending = false, - providers, - providersLoading = false, + runtimes, + runtimesLoading = false, onOpenChange, onSubmit, onImportUpdateFile, @@ -64,7 +64,7 @@ export function PersonaDialog({ const [displayName, setDisplayName] = React.useState(""); const [avatarUrl, setAvatarUrl] = React.useState(""); const [systemPrompt, setSystemPrompt] = React.useState(""); - const [provider, setProvider] = React.useState(""); + const [runtime, setRuntime] = React.useState(""); const [model, setModel] = React.useState(""); const [namePoolText, setNamePoolText] = React.useState(""); const [envVars, setEnvVars] = React.useState({}); @@ -88,7 +88,7 @@ export function PersonaDialog({ setDisplayName(initialValues.displayName); setAvatarUrl(initialValues.avatarUrl ?? ""); setSystemPrompt(initialValues.systemPrompt); - setProvider(initialValues.provider ?? ""); + setRuntime(initialValues.runtime ?? ""); setModel(initialValues.model ?? ""); setNamePoolText( ("namePool" in initialValues @@ -214,7 +214,7 @@ export function PersonaDialog({ setDisplayName(""); setAvatarUrl(""); setSystemPrompt(""); - setProvider(""); + setRuntime(""); setModel(""); setNamePoolText(""); setImportErrorMessage(null); @@ -238,7 +238,7 @@ export function PersonaDialog({ displayName, avatarUrl: avatarUrl.trim() || undefined, systemPrompt, - provider: provider.trim() || undefined, + runtime: runtime.trim() || undefined, model: model.trim() || undefined, namePool: namePool.length > 0 ? namePool : undefined, envVars, @@ -266,15 +266,15 @@ export function PersonaDialog({ importErrorMessage, }); - const selectedProvider = providers.find((p) => p.id === provider); - const providerWarning = - selectedProvider && selectedProvider.availability !== "available" ? ( + const selectedRuntime = runtimes.find((p) => p.id === runtime); + const runtimeWarning = + selectedRuntime && selectedRuntime.availability !== "available" ? (

- {selectedProvider.availability === "adapter_missing" - ? `${selectedProvider.label} CLI is installed but the ACP adapter is missing.` - : selectedProvider.availability === "cli_missing" - ? `${selectedProvider.label} ACP adapter is installed but the CLI is missing.` - : `${selectedProvider.label} is not installed.`}{" "} + {selectedRuntime.availability === "adapter_missing" + ? `${selectedRuntime.label} CLI is installed but the ACP adapter is missing.` + : selectedRuntime.availability === "cli_missing" + ? `${selectedRuntime.label} ACP adapter is installed but the CLI is missing.` + : `${selectedRuntime.label} is not installed.`}{" "} Visit Settings > Doctor to set it up.

) : null; @@ -349,22 +349,22 @@ export function PersonaDialog({
-