use std::{ any::Any, path::{Path, PathBuf}, sync::{Arc, LazyLock}, time::Duration,}; use anyhow::{Context as _, Result, bail};use collections::HashMap;use fs::{Fs, RemoveOptions};use futures::StreamExt;use gpui::{ AppContext as _, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task, TaskExt,};use http_client::{HttpClient, github::AssetKind};use node_runtime::NodeRuntime;use percent_encoding::percent_decode_str;use remote::RemoteClient;use rpc::{AnyProtoClient, TypedEnvelope, proto};use schemars::JsonSchema;use semver::Version;use serde::{Deserialize, Serialize};use settings::{AgentConfigOptionValue, RegisterSetting, SettingsStore, update_settings_file};use sha2::{Digest, Sha256};use url::Url;use util::{ResultExt as _, debug_panic}; use crate::ProjectEnvironment;use crate::agent_registry_store::{AgentRegistryStore, RegistryAgent, RegistryTargetConfig}; use crate::worktree_store::WorktreeStore; #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]pub struct AgentServerCommand { #[serde(rename = "command")] pub path: PathBuf, #[serde(default)] pub args: Vec<String>, pub env: Option<HashMap<String, String>>,} impl std::fmt::Debug for AgentServerCommand { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let filtered_env = self.env.as_ref().map(|env| { env.iter() .map(|(k, v)| { ( k, if util::redact::should_redact(k) { "[REDACTED]" } else { v }, ) }) .collect::<Vec<_>>() }); f.debug_struct("AgentServerCommand") .field("path", &self.path) .field("args", &self.args) .field("env", &filtered_env) .finish() }} #[derive( Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,)]#[serde(transparent)]pub struct AgentId(pub SharedString); impl AgentId { pub fn new(id: impl Into<SharedString>) -> Self { AgentId(id.into()) }} impl std::fmt::Display for AgentId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) }} impl From<&'static str> for AgentId { fn from(value: &'static str) -> Self { AgentId(value.into()) }} impl From<AgentId> for SharedString { fn from(value: AgentId) -> Self { value.0 }} impl AsRef<str> for AgentId { fn as_ref(&self) -> &str { &self.0 }} impl std::borrow::Borrow<str> for AgentId { fn borrow(&self) -> &str { &self.0 }} #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]pub enum ExternalAgentSource { #[default] Custom, Registry,} pub trait ExternalAgentServer { fn get_command( &mut self, extra_args: Vec<String>, extra_env: HashMap<String, String>, cx: &mut AsyncApp, ) -> Task<Result<AgentServerCommand>>; fn version(&self) -> Option<&SharedString> { None } fn take_new_version_available_tx(&mut self) -> Option<watch::Sender<Option<String>>> { None } fn set_new_version_available_tx(&mut self, _tx: watch::Sender<Option<String>>) {} fn take_loading_status_tx(&mut self) -> Option<watch::Sender<Option<String>>> { None } fn set_loading_status_tx(&mut self, _tx: watch::Sender<Option<String>>) {} fn as_any(&self) -> &dyn Any; fn as_any_mut(&mut self) -> &mut dyn Any;} enum AgentServerStoreState { Local { node_runtime: NodeRuntime, fs: Arc<dyn Fs>, project_environment: Entity<ProjectEnvironment>, downstream_client: Option<(u64, AnyProtoClient)>, settings: Option<AllAgentServersSettings>, http_client: Arc<dyn HttpClient>, _subscriptions: Vec<Subscription>, }, Remote { project_id: u64, upstream_client: Entity<RemoteClient>, worktree_store: Entity<WorktreeStore>, }, Collab,} pub struct ExternalAgentEntry { server: Box<dyn ExternalAgentServer>, icon: Option<SharedString>, display_name: Option<SharedString>, pub source: ExternalAgentSource,} impl ExternalAgentEntry { pub fn new( server: Box<dyn ExternalAgentServer>, source: ExternalAgentSource, icon: Option<SharedString>, display_name: Option<SharedString>, ) -> Self { Self { server, icon, display_name, source, } }} pub struct AgentServerStore { state: AgentServerStoreState, pub external_agents: HashMap<AgentId, ExternalAgentEntry>,} pub struct AgentServersUpdated; impl EventEmitter<AgentServersUpdated> for AgentServerStore {} static EXTENSION_TO_REGISTRY_IDS: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| { HashMap::from_iter([ ("opencode", "opencode"), ("mistral-vibe", "mistral-vibe"), ("auggie", "auggie"), ("stakpak", "stakpak"), ("codebuddy", "codebuddy-code"), ("autohand-acp", "autohand"), ("corust-agent", "corust-agent"), ("factory-droid", "factory-droid"), // Unmaintained // ("qqcode", ""), ]) }); impl AgentServerStore { pub fn migrate_agent_server_from_extensions( &mut self, id: Arc<str>, fs: Arc<dyn Fs>, cx: &mut Context<Self>, ) { let Some(registry_id) = EXTENSION_TO_REGISTRY_IDS.get(id.as_ref()) else { return; }; update_settings_file(fs, cx, move |settings, _| { let agent_servers = settings.agent_servers.get_or_insert_default(); // Take the old settings let settings = agent_servers.remove(id.as_ref()); // If they had both installed, just remove the extension settings, leave theirregistry settings alone if agent_servers.contains_key(*registry_id) { return; } // Insert the old settings, or write new ones so it is "installed" via the registry agent_servers.insert( registry_id.to_string(), settings.unwrap_or_else(|| settings::CustomAgentServerSettings::Registry { default_mode: None, env: Default::default(), default_config_options: HashMap::default(), favorite_config_option_values: HashMap::default(), }), ); }); } pub fn agent_icon(&self, id: &AgentId) -> Option<SharedString> { self.external_agents .get(id) .and_then(|entry| entry.icon.clone()) } pub fn agent_source(&self, name: &AgentId) -> Option<ExternalAgentSource> { self.external_agents.get(name).map(|entry| entry.source) }} impl AgentServerStore { pub fn agent_display_name(&self, name: &AgentId) -> Option<SharedString> { self.external_agents .get(name) .and_then(|entry| entry.display_name.clone()) } pub fn init_remote(session: &AnyProtoClient) { session.add_entity_message_handler(Self::handle_external_agents_updated); session.add_entity_message_handler(Self::handle_loading_status_updated); session.add_entity_message_handler(Self::handle_new_version_available); } pub fn init_headless(session: &AnyProtoClient) { session.add_entity_request_handler(Self::handle_get_agent_server_command); } fn agent_servers_settings_changed(&mut self, cx: &mut Context<Self>) { let AgentServerStoreState::Local { settings: old_settings, .. } = &mut self.state else { debug_panic!( "should not be subscribed to agent server settings changes in non-local project" ); return; }; let new_settings = cx .global::<SettingsStore>() .get::<AllAgentServersSettings>(None) .clone(); if Some(&new_settings) == old_settings.as_ref() { return; } self.reregister_agents(cx); } fn reregister_agents(&mut self, cx: &mut Context<Self>) { let AgentServerStoreState::Local { node_runtime, fs, project_environment, downstream_client, settings: old_settings, http_client, .. } = &mut self.state else { debug_panic!("Non-local projects should never attempt to reregister. This is a bug!"); return; }; let new_settings = cx .global::<SettingsStore>() .get::<AllAgentServersSettings>(None) .clone(); // If we don't have agents from the registry loaded yet, trigger a // refresh, which will cause this function to be called again let registry_store = AgentRegistryStore::try_global(cx); if new_settings.has_registry_agents() && let Some(registry) = registry_store.as_ref() { registry.update(cx, |registry, cx| registry.refresh_if_stale(cx)); } let registry_agents_by_id = registry_store .as_ref() .map(|store| { store .read(cx) .agents() .iter() .cloned() .map(|agent| (agent.id().to_string(), agent)) .collect::<HashMap<_, _>>() }) .unwrap_or_default(); // Drain the existing versioned agents, extracting reconnect state // from any active connection so we can preserve it or trigger a // reconnect when the version changes. let mut old_versioned_agents: HashMap< AgentId, ( SharedString, Option<watch::Sender<Option<String>>>, Option<watch::Sender<Option<String>>>, ), > = HashMap::default(); for (name, mut entry) in self.external_agents.drain() { if let Some(version) = entry.server.version().cloned() { let new_version_available_tx = entry.server.take_new_version_available_tx(); let loading_status_tx = entry.server.take_loading_status_tx(); if new_version_available_tx.is_some() || loading_status_tx.is_some() { old_versioned_agents .insert(name, (version, new_version_available_tx, loading_status_tx)); } } } for (name, settings) in new_settings.iter() { match settings { CustomAgentServerSettings::Custom { command, .. } => { let agent_name = AgentId(name.clone().into()); self.external_agents.insert( agent_name.clone(), ExternalAgentEntry::new( Box::new(LocalCustomAgent { command: command.clone(), project_environment: project_environment.clone(), }) as Box<dyn ExternalAgentServer>, ExternalAgentSource::Custom, None, None, ), ); } CustomAgentServerSettings::Registry { env, .. } => { let Some(agent) = registry_agents_by_id.get(name) else { if registry_store.is_some() { log::debug!("Registry agent '{}' not found in ACP registry", name); } continue; }; let agent_name = AgentId(name.clone().into()); match agent { RegistryAgent::Binary(agent) => { if !agent.supports_current_platform { log::warn!( "Registry agent '{}' has no compatible binary for this platform", name ); continue; } self.external_agents.insert( agent_name.clone(), ExternalAgentEntry::new( Box::new(LocalRegistryArchiveAgent { fs: fs.clone(), http_client: http_client.clone(), node_runtime: node_runtime.clone(), project_environment: project_environment.clone(), installation_dir: paths::external_agents_dir() .join("registry") .join(sanitize_path_component(name)), version: agent.metadata.version.clone(), targets: agent.targets.clone(), env: env.clone(), new_version_available_tx: None, loading_status_tx: None, }) as Box<dyn ExternalAgentServer>, ExternalAgentSource::Registry, agent.metadata.icon_path.clone(), Some(agent.metadata.name.clone()), ), ); } RegistryAgent::Npx(agent) => { self.external_agents.insert( agent_name.clone(), ExternalAgentEntry::new( Box::new(LocalRegistryNpxAgent { fs: fs.clone(), node_runtime: node_runtime.clone(), project_environment: project_environment.clone(), registry_id: Arc::from(name.as_str()), version: agent.metadata.version.clone(), package: agent.package.clone(), args: agent.args.clone(), distribution_env: agent.env.clone(), settings_env: env.clone(), new_version_available_tx: None, }) as Box<dyn ExternalAgentServer>, ExternalAgentSource::Registry, agent.metadata.icon_path.clone(), Some(agent.metadata.name.clone()), ), ); } } } } } // For each rebuilt versioned agent, compare the version. If it // changed, notify the active connection to reconnect. Otherwise, // transfer the channel to the new entry so future updates can use it. for (name, entry) in &mut self.external_agents { let Some((old_version, new_version_available_tx, loading_status_tx)) = old_versioned_agents.remove(name) else { continue; }; let Some(new_version) = entry.server.version() else { continue; }; if new_version != &old_version { if let Some(mut tx) = new_version_available_tx { tx.send(Some(new_version.to_string())).ok(); } } else { if let Some(tx) = new_version_available_tx { entry.server.set_new_version_available_tx(tx); } if let Some(tx) = loading_status_tx { entry.server.set_loading_status_tx(tx); } } } *old_settings = Some(new_settings); if let Some((project_id, downstream_client)) = downstream_client { downstream_client .send(proto::ExternalAgentsUpdated { project_id: *project_id, names: self .external_agents .keys() .map(|name| name.to_string()) .collect(), }) .log_err(); } cx.emit(AgentServersUpdated); } pub fn node_runtime(&self) -> Option<NodeRuntime> { match &self.state { AgentServerStoreState::Local { node_runtime, .. } => Some(node_runtime.clone()), _ => None, } } pub fn local( node_runtime: NodeRuntime, fs: Arc<dyn Fs>,Lines 1–500 of 2340. Load more
use std::{ any::Any, path::{Path, PathBuf}, sync::{Arc, LazyLock}, time::Duration, }; use anyhow::{Context as _, Result, bail}; use collections::HashMap; use fs::{Fs, RemoveOptions}; use futures::StreamExt; use gpui::{ AppContext as _, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task, TaskExt, }; use http_client::{HttpClient, github::AssetKind}; use node_runtime::NodeRuntime; use percent_encoding::percent_decode_str; use remote::RemoteClient; use rpc::{AnyProtoClient, TypedEnvelope, proto}; use schemars::JsonSchema; use semver::Version; use serde::{Deserialize, Serialize}; use settings::{AgentConfigOptionValue, RegisterSetting, SettingsStore, update_settings_file}; use sha2::{Digest, Sha256}; use url::Url; use util::{ResultExt as _, debug_panic}; use crate::ProjectEnvironment; use crate::agent_registry_store::{AgentRegistryStore, RegistryAgent, RegistryTargetConfig}; use crate::worktree_store::WorktreeStore; #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)] pub struct AgentServerCommand { #[serde(rename = "command")] pub path: PathBuf, #[serde(default)] pub args: Vec<String>, pub env: Option<HashMap<String, String>>, } impl std::fmt::Debug for AgentServerCommand { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let filtered_env = self.env.as_ref().map(|env| { env.iter() .map(|(k, v)| { ( k, if util::redact::should_redact(k) { "[REDACTED]" } else { v }, ) }) .collect::<Vec<_>>() }); f.debug_struct("AgentServerCommand") .field("path", &self.path) .field("args", &self.args) .field("env", &filtered_env) .finish() } } #[derive( Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, )] #[serde(transparent)] pub struct AgentId(pub SharedString); impl AgentId { pub fn new(id: impl Into<SharedString>) -> Self { AgentId(id.into()) } } impl std::fmt::Display for AgentId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } impl From<&'static str> for AgentId { fn from(value: &'static str) -> Self { AgentId(value.into()) } } impl From<AgentId> for SharedString { fn from(value: AgentId) -> Self { value.0 } } impl AsRef<str> for AgentId { fn as_ref(&self) -> &str { &self.0 } } impl std::borrow::Borrow<str> for AgentId { fn borrow(&self) -> &str { &self.0 } } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum ExternalAgentSource { #[default] Custom, Registry, } pub trait ExternalAgentServer { fn get_command( &mut self, extra_args: Vec<String>, extra_env: HashMap<String, String>, cx: &mut AsyncApp, ) -> Task<Result<AgentServerCommand>>; fn version(&self) -> Option<&SharedString> { None } fn take_new_version_available_tx(&mut self) -> Option<watch::Sender<Option<String>>> { None } fn set_new_version_available_tx(&mut self, _tx: watch::Sender<Option<String>>) {} fn take_loading_status_tx(&mut self) -> Option<watch::Sender<Option<String>>> { None } fn set_loading_status_tx(&mut self, _tx: watch::Sender<Option<String>>) {} fn as_any(&self) -> &dyn Any; fn as_any_mut(&mut self) -> &mut dyn Any; } enum AgentServerStoreState { Local { node_runtime: NodeRuntime, fs: Arc<dyn Fs>, project_environment: Entity<ProjectEnvironment>, downstream_client: Option<(u64, AnyProtoClient)>, settings: Option<AllAgentServersSettings>, http_client: Arc<dyn HttpClient>, _subscriptions: Vec<Subscription>, }, Remote { project_id: u64, upstream_client: Entity<RemoteClient>, worktree_store: Entity<WorktreeStore>, }, Collab, } pub struct ExternalAgentEntry { server: Box<dyn ExternalAgentServer>, icon: Option<SharedString>, display_name: Option<SharedString>, pub source: ExternalAgentSource, } impl ExternalAgentEntry { pub fn new( server: Box<dyn ExternalAgentServer>, source: ExternalAgentSource, icon: Option<SharedString>, display_name: Option<SharedString>, ) -> Self { Self { server, icon, display_name, source, } } } pub struct AgentServerStore { state: AgentServerStoreState, pub external_agents: HashMap<AgentId, ExternalAgentEntry>, } pub struct AgentServersUpdated; impl EventEmitter<AgentServersUpdated> for AgentServerStore {} static EXTENSION_TO_REGISTRY_IDS: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| { HashMap::from_iter([ ("opencode", "opencode"), ("mistral-vibe", "mistral-vibe"), ("auggie", "auggie"), ("stakpak", "stakpak"), ("codebuddy", "codebuddy-code"), ("autohand-acp", "autohand"), ("corust-agent", "corust-agent"), ("factory-droid", "factory-droid"), // Unmaintained // ("qqcode", ""), ]) }); impl AgentServerStore { pub fn migrate_agent_server_from_extensions( &mut self, id: Arc<str>, fs: Arc<dyn Fs>, cx: &mut Context<Self>, ) { let Some(registry_id) = EXTENSION_TO_REGISTRY_IDS.get(id.as_ref()) else { return; }; update_settings_file(fs, cx, move |settings, _| { let agent_servers = settings.agent_servers.get_or_insert_default(); // Take the old settings let settings = agent_servers.remove(id.as_ref()); // If they had both installed, just remove the extension settings, leave theirregistry settings alone if agent_servers.contains_key(*registry_id) { return; } // Insert the old settings, or write new ones so it is "installed" via the registry agent_servers.insert( registry_id.to_string(), settings.unwrap_or_else(|| settings::CustomAgentServerSettings::Registry { default_mode: None, env: Default::default(), default_config_options: HashMap::default(), favorite_config_option_values: HashMap::default(), }), ); }); } pub fn agent_icon(&self, id: &AgentId) -> Option<SharedString> { self.external_agents .get(id) .and_then(|entry| entry.icon.clone()) } pub fn agent_source(&self, name: &AgentId) -> Option<ExternalAgentSource> { self.external_agents.get(name).map(|entry| entry.source) } } impl AgentServerStore { pub fn agent_display_name(&self, name: &AgentId) -> Option<SharedString> { self.external_agents .get(name) .and_then(|entry| entry.display_name.clone()) } pub fn init_remote(session: &AnyProtoClient) { session.add_entity_message_handler(Self::handle_external_agents_updated); session.add_entity_message_handler(Self::handle_loading_status_updated); session.add_entity_message_handler(Self::handle_new_version_available); } pub fn init_headless(session: &AnyProtoClient) { session.add_entity_request_handler(Self::handle_get_agent_server_command); } fn agent_servers_settings_changed(&mut self, cx: &mut Context<Self>) { let AgentServerStoreState::Local { settings: old_settings, .. } = &mut self.state else { debug_panic!( "should not be subscribed to agent server settings changes in non-local project" ); return; }; let new_settings = cx .global::<SettingsStore>() .get::<AllAgentServersSettings>(None) .clone(); if Some(&new_settings) == old_settings.as_ref() { return; } self.reregister_agents(cx); } fn reregister_agents(&mut self, cx: &mut Context<Self>) { let AgentServerStoreState::Local { node_runtime, fs, project_environment, downstream_client, settings: old_settings, http_client, .. } = &mut self.state else { debug_panic!("Non-local projects should never attempt to reregister. This is a bug!"); return; }; let new_settings = cx .global::<SettingsStore>() .get::<AllAgentServersSettings>(None) .clone(); // If we don't have agents from the registry loaded yet, trigger a // refresh, which will cause this function to be called again let registry_store = AgentRegistryStore::try_global(cx); if new_settings.has_registry_agents() && let Some(registry) = registry_store.as_ref() { registry.update(cx, |registry, cx| registry.refresh_if_stale(cx)); } let registry_agents_by_id = registry_store .as_ref() .map(|store| { store .read(cx) .agents() .iter() .cloned() .map(|agent| (agent.id().to_string(), agent)) .collect::<HashMap<_, _>>() }) .unwrap_or_default(); // Drain the existing versioned agents, extracting reconnect state // from any active connection so we can preserve it or trigger a // reconnect when the version changes. let mut old_versioned_agents: HashMap< AgentId, ( SharedString, Option<watch::Sender<Option<String>>>, Option<watch::Sender<Option<String>>>, ), > = HashMap::default(); for (name, mut entry) in self.external_agents.drain() { if let Some(version) = entry.server.version().cloned() { let new_version_available_tx = entry.server.take_new_version_available_tx(); let loading_status_tx = entry.server.take_loading_status_tx(); if new_version_available_tx.is_some() || loading_status_tx.is_some() { old_versioned_agents .insert(name, (version, new_version_available_tx, loading_status_tx)); } } } for (name, settings) in new_settings.iter() { match settings { CustomAgentServerSettings::Custom { command, .. } => { let agent_name = AgentId(name.clone().into()); self.external_agents.insert( agent_name.clone(), ExternalAgentEntry::new( Box::new(LocalCustomAgent { command: command.clone(), project_environment: project_environment.clone(), }) as Box<dyn ExternalAgentServer>, ExternalAgentSource::Custom, None, None, ), ); } CustomAgentServerSettings::Registry { env, .. } => { let Some(agent) = registry_agents_by_id.get(name) else { if registry_store.is_some() { log::debug!("Registry agent '{}' not found in ACP registry", name); } continue; }; let agent_name = AgentId(name.clone().into()); match agent { RegistryAgent::Binary(agent) => { if !agent.supports_current_platform { log::warn!( "Registry agent '{}' has no compatible binary for this platform", name ); continue; } self.external_agents.insert( agent_name.clone(), ExternalAgentEntry::new( Box::new(LocalRegistryArchiveAgent { fs: fs.clone(), http_client: http_client.clone(), node_runtime: node_runtime.clone(), project_environment: project_environment.clone(), installation_dir: paths::external_agents_dir() .join("registry") .join(sanitize_path_component(name)), version: agent.metadata.version.clone(), targets: agent.targets.clone(), env: env.clone(), new_version_available_tx: None, loading_status_tx: None, }) as Box<dyn ExternalAgentServer>, ExternalAgentSource::Registry, agent.metadata.icon_path.clone(), Some(agent.metadata.name.clone()), ), ); } RegistryAgent::Npx(agent) => { self.external_agents.insert( agent_name.clone(), ExternalAgentEntry::new( Box::new(LocalRegistryNpxAgent { fs: fs.clone(), node_runtime: node_runtime.clone(), project_environment: project_environment.clone(), registry_id: Arc::from(name.as_str()), version: agent.metadata.version.clone(), package: agent.package.clone(), args: agent.args.clone(), distribution_env: agent.env.clone(), settings_env: env.clone(), new_version_available_tx: None, }) as Box<dyn ExternalAgentServer>, ExternalAgentSource::Registry, agent.metadata.icon_path.clone(), Some(agent.metadata.name.clone()), ), ); } } } } } // For each rebuilt versioned agent, compare the version. If it // changed, notify the active connection to reconnect. Otherwise, // transfer the channel to the new entry so future updates can use it. for (name, entry) in &mut self.external_agents { let Some((old_version, new_version_available_tx, loading_status_tx)) = old_versioned_agents.remove(name) else { continue; }; let Some(new_version) = entry.server.version() else { continue; }; if new_version != &old_version { if let Some(mut tx) = new_version_available_tx { tx.send(Some(new_version.to_string())).ok(); } } else { if let Some(tx) = new_version_available_tx { entry.server.set_new_version_available_tx(tx); } if let Some(tx) = loading_status_tx { entry.server.set_loading_status_tx(tx); } } } *old_settings = Some(new_settings); if let Some((project_id, downstream_client)) = downstream_client { downstream_client .send(proto::ExternalAgentsUpdated { project_id: *project_id, names: self .external_agents .keys() .map(|name| name.to_string()) .collect(), }) .log_err(); } cx.emit(AgentServersUpdated); } pub fn node_runtime(&self) -> Option<NodeRuntime> { match &self.state { AgentServerStoreState::Local { node_runtime, .. } => Some(node_runtime.clone()), _ => None, } } pub fn local( node_runtime: NodeRuntime, fs: Arc<dyn Fs>,