connection.rs

  1use crate::AcpThread;
  2use agent_client_protocol::{self as acp};
  3use anyhow::Result;
  4use chrono::{DateTime, Utc};
  5use collections::IndexMap;
  6use gpui::{Entity, SharedString, Task};
  7use language_model::LanguageModelProviderId;
  8use project::Project;
  9use serde::{Deserialize, Serialize};
 10use std::{
 11    any::Any,
 12    error::Error,
 13    fmt,
 14    path::{Path, PathBuf},
 15    rc::Rc,
 16    sync::Arc,
 17};
 18use ui::{App, IconName};
 19use uuid::Uuid;
 20
 21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
 22pub struct UserMessageId(Arc<str>);
 23
 24impl UserMessageId {
 25    pub fn new() -> Self {
 26        Self(Uuid::new_v4().to_string().into())
 27    }
 28}
 29
 30pub trait AgentConnection {
 31    fn telemetry_id(&self) -> SharedString;
 32
 33    fn new_thread(
 34        self: Rc<Self>,
 35        project: Entity<Project>,
 36        cwd: &Path,
 37        cx: &mut App,
 38    ) -> Task<Result<Entity<AcpThread>>>;
 39
 40    /// Whether this agent supports loading existing sessions.
 41    fn supports_load_session(&self, _cx: &App) -> bool {
 42        false
 43    }
 44
 45    /// Load an existing session by ID.
 46    fn load_session(
 47        self: Rc<Self>,
 48        _session: AgentSessionInfo,
 49        _project: Entity<Project>,
 50        _cwd: &Path,
 51        _cx: &mut App,
 52    ) -> Task<Result<Entity<AcpThread>>> {
 53        Task::ready(Err(anyhow::Error::msg("Loading sessions is not supported")))
 54    }
 55
 56    /// Whether this agent supports resuming existing sessions without loading history.
 57    fn supports_resume_session(&self, _cx: &App) -> bool {
 58        false
 59    }
 60
 61    /// Resume an existing session by ID without replaying previous messages.
 62    fn resume_session(
 63        self: Rc<Self>,
 64        _session: AgentSessionInfo,
 65        _project: Entity<Project>,
 66        _cwd: &Path,
 67        _cx: &mut App,
 68    ) -> Task<Result<Entity<AcpThread>>> {
 69        Task::ready(Err(anyhow::Error::msg(
 70            "Resuming sessions is not supported",
 71        )))
 72    }
 73
 74    /// Whether this agent supports showing session history.
 75    fn supports_session_history(&self, cx: &App) -> bool {
 76        self.supports_load_session(cx) || self.supports_resume_session(cx)
 77    }
 78
 79    fn auth_methods(&self) -> &[acp::AuthMethod];
 80
 81    fn authenticate(&self, method: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>>;
 82
 83    fn prompt(
 84        &self,
 85        user_message_id: Option<UserMessageId>,
 86        params: acp::PromptRequest,
 87        cx: &mut App,
 88    ) -> Task<Result<acp::PromptResponse>>;
 89
 90    fn retry(&self, _session_id: &acp::SessionId, _cx: &App) -> Option<Rc<dyn AgentSessionRetry>> {
 91        None
 92    }
 93
 94    fn cancel(&self, session_id: &acp::SessionId, cx: &mut App);
 95
 96    fn truncate(
 97        &self,
 98        _session_id: &acp::SessionId,
 99        _cx: &App,
100    ) -> Option<Rc<dyn AgentSessionTruncate>> {
101        None
102    }
103
104    fn set_title(
105        &self,
106        _session_id: &acp::SessionId,
107        _cx: &App,
108    ) -> Option<Rc<dyn AgentSessionSetTitle>> {
109        None
110    }
111
112    /// Returns this agent as an [Rc<dyn ModelSelector>] if the model selection capability is supported.
113    ///
114    /// If the agent does not support model selection, returns [None].
115    /// This allows sharing the selector in UI components.
116    fn model_selector(&self, _session_id: &acp::SessionId) -> Option<Rc<dyn AgentModelSelector>> {
117        None
118    }
119
120    fn telemetry(&self) -> Option<Rc<dyn AgentTelemetry>> {
121        None
122    }
123
124    fn session_modes(
125        &self,
126        _session_id: &acp::SessionId,
127        _cx: &App,
128    ) -> Option<Rc<dyn AgentSessionModes>> {
129        None
130    }
131
132    fn session_config_options(
133        &self,
134        _session_id: &acp::SessionId,
135        _cx: &App,
136    ) -> Option<Rc<dyn AgentSessionConfigOptions>> {
137        None
138    }
139
140    fn session_list(&self, _cx: &mut App) -> Option<Rc<dyn AgentSessionList>> {
141        None
142    }
143
144    fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
145}
146
147impl dyn AgentConnection {
148    pub fn downcast<T: 'static + AgentConnection + Sized>(self: Rc<Self>) -> Option<Rc<T>> {
149        self.into_any().downcast().ok()
150    }
151}
152
153pub trait AgentSessionTruncate {
154    fn run(&self, message_id: UserMessageId, cx: &mut App) -> Task<Result<()>>;
155}
156
157pub trait AgentSessionRetry {
158    fn run(&self, cx: &mut App) -> Task<Result<acp::PromptResponse>>;
159}
160
161pub trait AgentSessionSetTitle {
162    fn run(&self, title: SharedString, cx: &mut App) -> Task<Result<()>>;
163}
164
165pub trait AgentTelemetry {
166    /// A representation of the current thread state that can be serialized for
167    /// storage with telemetry events.
168    fn thread_data(
169        &self,
170        session_id: &acp::SessionId,
171        cx: &mut App,
172    ) -> Task<Result<serde_json::Value>>;
173}
174
175pub trait AgentSessionModes {
176    fn current_mode(&self) -> acp::SessionModeId;
177
178    fn all_modes(&self) -> Vec<acp::SessionMode>;
179
180    fn set_mode(&self, mode: acp::SessionModeId, cx: &mut App) -> Task<Result<()>>;
181}
182
183pub trait AgentSessionConfigOptions {
184    /// Get all current config options with their state
185    fn config_options(&self) -> Vec<acp::SessionConfigOption>;
186
187    /// Set a config option value
188    /// Returns the full updated list of config options
189    fn set_config_option(
190        &self,
191        config_id: acp::SessionConfigId,
192        value: acp::SessionConfigValueId,
193        cx: &mut App,
194    ) -> Task<Result<Vec<acp::SessionConfigOption>>>;
195
196    /// Whenever the config options are updated the receiver will be notified.
197    /// Optional for agents that don't update their config options dynamically.
198    fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> {
199        None
200    }
201}
202
203#[derive(Debug, Clone, Default)]
204pub struct AgentSessionListRequest {
205    pub cwd: Option<PathBuf>,
206    pub cursor: Option<String>,
207    pub meta: Option<acp::Meta>,
208}
209
210#[derive(Debug, Clone)]
211pub struct AgentSessionListResponse {
212    pub sessions: Vec<AgentSessionInfo>,
213    pub next_cursor: Option<String>,
214    pub meta: Option<acp::Meta>,
215}
216
217impl AgentSessionListResponse {
218    pub fn new(sessions: Vec<AgentSessionInfo>) -> Self {
219        Self {
220            sessions,
221            next_cursor: None,
222            meta: None,
223        }
224    }
225}
226
227#[derive(Debug, Clone, PartialEq)]
228pub struct AgentSessionInfo {
229    pub session_id: acp::SessionId,
230    pub cwd: Option<PathBuf>,
231    pub title: Option<SharedString>,
232    pub updated_at: Option<DateTime<Utc>>,
233    pub meta: Option<acp::Meta>,
234}
235
236impl AgentSessionInfo {
237    pub fn new(session_id: impl Into<acp::SessionId>) -> Self {
238        Self {
239            session_id: session_id.into(),
240            cwd: None,
241            title: None,
242            updated_at: None,
243            meta: None,
244        }
245    }
246}
247
248#[derive(Debug, Clone)]
249pub enum SessionListUpdate {
250    Refresh,
251    SessionInfo {
252        session_id: acp::SessionId,
253        update: acp::SessionInfoUpdate,
254    },
255}
256
257pub trait AgentSessionList {
258    fn list_sessions(
259        &self,
260        request: AgentSessionListRequest,
261        cx: &mut App,
262    ) -> Task<Result<AgentSessionListResponse>>;
263
264    fn supports_delete(&self) -> bool {
265        false
266    }
267
268    fn delete_session(&self, _session_id: &acp::SessionId, _cx: &mut App) -> Task<Result<()>> {
269        Task::ready(Err(anyhow::anyhow!("delete_session not supported")))
270    }
271
272    fn delete_sessions(&self, _cx: &mut App) -> Task<Result<()>> {
273        Task::ready(Err(anyhow::anyhow!("delete_sessions not supported")))
274    }
275
276    fn watch(&self, _cx: &mut App) -> Option<smol::channel::Receiver<SessionListUpdate>> {
277        None
278    }
279
280    fn notify_refresh(&self) {}
281
282    fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
283}
284
285impl dyn AgentSessionList {
286    pub fn downcast<T: 'static + AgentSessionList + Sized>(self: Rc<Self>) -> Option<Rc<T>> {
287        self.into_any().downcast().ok()
288    }
289}
290
291#[derive(Debug)]
292pub struct AuthRequired {
293    pub description: Option<String>,
294    pub provider_id: Option<LanguageModelProviderId>,
295}
296
297impl AuthRequired {
298    pub fn new() -> Self {
299        Self {
300            description: None,
301            provider_id: None,
302        }
303    }
304
305    pub fn with_description(mut self, description: String) -> Self {
306        self.description = Some(description);
307        self
308    }
309
310    pub fn with_language_model_provider(mut self, provider_id: LanguageModelProviderId) -> Self {
311        self.provider_id = Some(provider_id);
312        self
313    }
314}
315
316impl Error for AuthRequired {}
317impl fmt::Display for AuthRequired {
318    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
319        write!(f, "Authentication required")
320    }
321}
322
323/// Trait for agents that support listing, selecting, and querying language models.
324///
325/// This is an optional capability; agents indicate support via [AgentConnection::model_selector].
326pub trait AgentModelSelector: 'static {
327    /// Lists all available language models for this agent.
328    ///
329    /// # Parameters
330    /// - `cx`: The GPUI app context for async operations and global access.
331    ///
332    /// # Returns
333    /// A task resolving to the list of models or an error (e.g., if no models are configured).
334    fn list_models(&self, cx: &mut App) -> Task<Result<AgentModelList>>;
335
336    /// Selects a model for a specific session (thread).
337    ///
338    /// This sets the default model for future interactions in the session.
339    /// If the session doesn't exist or the model is invalid, it returns an error.
340    ///
341    /// # Parameters
342    /// - `model`: The model to select (should be one from [list_models]).
343    /// - `cx`: The GPUI app context.
344    ///
345    /// # Returns
346    /// A task resolving to `Ok(())` on success or an error.
347    fn select_model(&self, model_id: acp::ModelId, cx: &mut App) -> Task<Result<()>>;
348
349    /// Retrieves the currently selected model for a specific session (thread).
350    ///
351    /// # Parameters
352    /// - `cx`: The GPUI app context.
353    ///
354    /// # Returns
355    /// A task resolving to the selected model (always set) or an error (e.g., session not found).
356    fn selected_model(&self, cx: &mut App) -> Task<Result<AgentModelInfo>>;
357
358    /// Whenever the model list is updated the receiver will be notified.
359    /// Optional for agents that don't update their model list.
360    fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> {
361        None
362    }
363
364    /// Returns whether the model picker should render a footer.
365    fn should_render_footer(&self) -> bool {
366        false
367    }
368}
369
370/// Icon for a model in the model selector.
371#[derive(Debug, Clone, PartialEq, Eq)]
372pub enum AgentModelIcon {
373    /// A built-in icon from Zed's icon set.
374    Named(IconName),
375    /// Path to a custom SVG icon file.
376    Path(SharedString),
377}
378
379#[derive(Debug, Clone, PartialEq, Eq)]
380pub struct AgentModelInfo {
381    pub id: acp::ModelId,
382    pub name: SharedString,
383    pub description: Option<SharedString>,
384    pub icon: Option<AgentModelIcon>,
385    pub is_latest: bool,
386}
387
388impl From<acp::ModelInfo> for AgentModelInfo {
389    fn from(info: acp::ModelInfo) -> Self {
390        Self {
391            id: info.model_id,
392            name: info.name.into(),
393            description: info.description.map(|desc| desc.into()),
394            icon: None,
395            is_latest: false,
396        }
397    }
398}
399
400#[derive(Debug, Clone, PartialEq, Eq, Hash)]
401pub struct AgentModelGroupName(pub SharedString);
402
403#[derive(Debug, Clone)]
404pub enum AgentModelList {
405    Flat(Vec<AgentModelInfo>),
406    Grouped(IndexMap<AgentModelGroupName, Vec<AgentModelInfo>>),
407}
408
409impl AgentModelList {
410    pub fn is_empty(&self) -> bool {
411        match self {
412            AgentModelList::Flat(models) => models.is_empty(),
413            AgentModelList::Grouped(groups) => groups.is_empty(),
414        }
415    }
416
417    pub fn is_flat(&self) -> bool {
418        matches!(self, AgentModelList::Flat(_))
419    }
420}
421
422#[derive(Debug, Clone)]
423pub struct PermissionOptionChoice {
424    pub allow: acp::PermissionOption,
425    pub deny: acp::PermissionOption,
426}
427
428impl PermissionOptionChoice {
429    pub fn label(&self) -> SharedString {
430        self.allow.name.clone().into()
431    }
432}
433
434#[derive(Debug, Clone)]
435pub enum PermissionOptions {
436    Flat(Vec<acp::PermissionOption>),
437    Dropdown(Vec<PermissionOptionChoice>),
438}
439
440impl PermissionOptions {
441    pub fn is_empty(&self) -> bool {
442        match self {
443            PermissionOptions::Flat(options) => options.is_empty(),
444            PermissionOptions::Dropdown(options) => options.is_empty(),
445        }
446    }
447
448    pub fn first_option_of_kind(
449        &self,
450        kind: acp::PermissionOptionKind,
451    ) -> Option<&acp::PermissionOption> {
452        match self {
453            PermissionOptions::Flat(options) => options.iter().find(|option| option.kind == kind),
454            PermissionOptions::Dropdown(options) => options.iter().find_map(|choice| {
455                if choice.allow.kind == kind {
456                    Some(&choice.allow)
457                } else if choice.deny.kind == kind {
458                    Some(&choice.deny)
459                } else {
460                    None
461                }
462            }),
463        }
464    }
465
466    pub fn allow_once_option_id(&self) -> Option<acp::PermissionOptionId> {
467        self.first_option_of_kind(acp::PermissionOptionKind::AllowOnce)
468            .map(|option| option.option_id.clone())
469    }
470}
471
472#[cfg(feature = "test-support")]
473mod test_support {
474    //! Test-only stubs and helpers for acp_thread.
475    //!
476    //! This module is gated by the `test-support` feature and is not included
477    //! in production builds. It provides:
478    //! - `StubAgentConnection` for mocking agent connections in tests
479    //! - `create_test_png_base64` for generating test images
480
481    use std::sync::Arc;
482
483    use action_log::ActionLog;
484    use collections::HashMap;
485    use futures::{channel::oneshot, future::try_join_all};
486    use gpui::{AppContext as _, WeakEntity};
487    use parking_lot::Mutex;
488
489    use super::*;
490
491    /// Creates a PNG image encoded as base64 for testing.
492    ///
493    /// Generates a solid-color PNG of the specified dimensions and returns
494    /// it as a base64-encoded string suitable for use in `ImageContent`.
495    pub fn create_test_png_base64(width: u32, height: u32, color: [u8; 4]) -> String {
496        use image::ImageEncoder as _;
497
498        let mut png_data = Vec::new();
499        {
500            let encoder = image::codecs::png::PngEncoder::new(&mut png_data);
501            let mut pixels = Vec::with_capacity((width * height * 4) as usize);
502            for _ in 0..(width * height) {
503                pixels.extend_from_slice(&color);
504            }
505            encoder
506                .write_image(&pixels, width, height, image::ExtendedColorType::Rgba8)
507                .expect("Failed to encode PNG");
508        }
509
510        use image::EncodableLayout as _;
511        base64::Engine::encode(
512            &base64::engine::general_purpose::STANDARD,
513            png_data.as_bytes(),
514        )
515    }
516
517    #[derive(Clone, Default)]
518    pub struct StubAgentConnection {
519        sessions: Arc<Mutex<HashMap<acp::SessionId, Session>>>,
520        permission_requests: HashMap<acp::ToolCallId, PermissionOptions>,
521        next_prompt_updates: Arc<Mutex<Vec<acp::SessionUpdate>>>,
522    }
523
524    struct Session {
525        thread: WeakEntity<AcpThread>,
526        response_tx: Option<oneshot::Sender<acp::StopReason>>,
527    }
528
529    impl StubAgentConnection {
530        pub fn new() -> Self {
531            Self {
532                next_prompt_updates: Default::default(),
533                permission_requests: HashMap::default(),
534                sessions: Arc::default(),
535            }
536        }
537
538        pub fn set_next_prompt_updates(&self, updates: Vec<acp::SessionUpdate>) {
539            *self.next_prompt_updates.lock() = updates;
540        }
541
542        pub fn with_permission_requests(
543            mut self,
544            permission_requests: HashMap<acp::ToolCallId, PermissionOptions>,
545        ) -> Self {
546            self.permission_requests = permission_requests;
547            self
548        }
549
550        pub fn send_update(
551            &self,
552            session_id: acp::SessionId,
553            update: acp::SessionUpdate,
554            cx: &mut App,
555        ) {
556            assert!(
557                self.next_prompt_updates.lock().is_empty(),
558                "Use either send_update or set_next_prompt_updates"
559            );
560
561            self.sessions
562                .lock()
563                .get(&session_id)
564                .unwrap()
565                .thread
566                .update(cx, |thread, cx| {
567                    thread.handle_session_update(update, cx).unwrap();
568                })
569                .unwrap();
570        }
571
572        pub fn end_turn(&self, session_id: acp::SessionId, stop_reason: acp::StopReason) {
573            self.sessions
574                .lock()
575                .get_mut(&session_id)
576                .unwrap()
577                .response_tx
578                .take()
579                .expect("No pending turn")
580                .send(stop_reason)
581                .unwrap();
582        }
583    }
584
585    impl AgentConnection for StubAgentConnection {
586        fn telemetry_id(&self) -> SharedString {
587            "stub".into()
588        }
589
590        fn auth_methods(&self) -> &[acp::AuthMethod] {
591            &[]
592        }
593
594        fn model_selector(
595            &self,
596            _session_id: &acp::SessionId,
597        ) -> Option<Rc<dyn AgentModelSelector>> {
598            Some(self.model_selector_impl())
599        }
600
601        fn new_thread(
602            self: Rc<Self>,
603            project: Entity<Project>,
604            _cwd: &Path,
605            cx: &mut gpui::App,
606        ) -> Task<gpui::Result<Entity<AcpThread>>> {
607            let session_id = acp::SessionId::new(self.sessions.lock().len().to_string());
608            let action_log = cx.new(|_| ActionLog::new(project.clone()));
609            let thread = cx.new(|cx| {
610                AcpThread::new(
611                    "Test",
612                    self.clone(),
613                    project,
614                    action_log,
615                    session_id.clone(),
616                    watch::Receiver::constant(
617                        acp::PromptCapabilities::new()
618                            .image(true)
619                            .audio(true)
620                            .embedded_context(true),
621                    ),
622                    cx,
623                )
624            });
625            self.sessions.lock().insert(
626                session_id,
627                Session {
628                    thread: thread.downgrade(),
629                    response_tx: None,
630                },
631            );
632            Task::ready(Ok(thread))
633        }
634
635        fn authenticate(
636            &self,
637            _method_id: acp::AuthMethodId,
638            _cx: &mut App,
639        ) -> Task<gpui::Result<()>> {
640            unimplemented!()
641        }
642
643        fn prompt(
644            &self,
645            _id: Option<UserMessageId>,
646            params: acp::PromptRequest,
647            cx: &mut App,
648        ) -> Task<gpui::Result<acp::PromptResponse>> {
649            let mut sessions = self.sessions.lock();
650            let Session {
651                thread,
652                response_tx,
653            } = sessions.get_mut(&params.session_id).unwrap();
654            let mut tasks = vec![];
655            if self.next_prompt_updates.lock().is_empty() {
656                let (tx, rx) = oneshot::channel();
657                response_tx.replace(tx);
658                cx.spawn(async move |_| {
659                    let stop_reason = rx.await?;
660                    Ok(acp::PromptResponse::new(stop_reason))
661                })
662            } else {
663                for update in self.next_prompt_updates.lock().drain(..) {
664                    let thread = thread.clone();
665                    let update = update.clone();
666                    let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) =
667                        &update
668                        && let Some(options) = self.permission_requests.get(&tool_call.tool_call_id)
669                    {
670                        Some((tool_call.clone(), options.clone()))
671                    } else {
672                        None
673                    };
674                    let task = cx.spawn(async move |cx| {
675                        if let Some((tool_call, options)) = permission_request {
676                            thread
677                                .update(cx, |thread, cx| {
678                                    thread.request_tool_call_authorization(
679                                        tool_call.clone().into(),
680                                        options.clone(),
681                                        false,
682                                        cx,
683                                    )
684                                })??
685                                .await;
686                        }
687                        thread.update(cx, |thread, cx| {
688                            thread.handle_session_update(update.clone(), cx).unwrap();
689                        })?;
690                        anyhow::Ok(())
691                    });
692                    tasks.push(task);
693                }
694
695                cx.spawn(async move |_| {
696                    try_join_all(tasks).await?;
697                    Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
698                })
699            }
700        }
701
702        fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
703            if let Some(end_turn_tx) = self
704                .sessions
705                .lock()
706                .get_mut(session_id)
707                .unwrap()
708                .response_tx
709                .take()
710            {
711                end_turn_tx.send(acp::StopReason::Cancelled).unwrap();
712            }
713        }
714
715        fn truncate(
716            &self,
717            _session_id: &agent_client_protocol::SessionId,
718            _cx: &App,
719        ) -> Option<Rc<dyn AgentSessionTruncate>> {
720            Some(Rc::new(StubAgentSessionEditor))
721        }
722
723        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
724            self
725        }
726    }
727
728    struct StubAgentSessionEditor;
729
730    impl AgentSessionTruncate for StubAgentSessionEditor {
731        fn run(&self, _: UserMessageId, _: &mut App) -> Task<Result<()>> {
732            Task::ready(Ok(()))
733        }
734    }
735
736    #[derive(Clone)]
737    struct StubModelSelector {
738        selected_model: Arc<Mutex<AgentModelInfo>>,
739    }
740
741    impl StubModelSelector {
742        fn new() -> Self {
743            Self {
744                selected_model: Arc::new(Mutex::new(AgentModelInfo {
745                    id: acp::ModelId::new("visual-test-model"),
746                    name: "Visual Test Model".into(),
747                    description: Some("A stub model for visual testing".into()),
748                    icon: Some(AgentModelIcon::Named(ui::IconName::ZedAssistant)),
749                    is_latest: false,
750                })),
751            }
752        }
753    }
754
755    impl AgentModelSelector for StubModelSelector {
756        fn list_models(&self, _cx: &mut App) -> Task<Result<AgentModelList>> {
757            let model = self.selected_model.lock().clone();
758            Task::ready(Ok(AgentModelList::Flat(vec![model])))
759        }
760
761        fn select_model(&self, model_id: acp::ModelId, _cx: &mut App) -> Task<Result<()>> {
762            self.selected_model.lock().id = model_id;
763            Task::ready(Ok(()))
764        }
765
766        fn selected_model(&self, _cx: &mut App) -> Task<Result<AgentModelInfo>> {
767            Task::ready(Ok(self.selected_model.lock().clone()))
768        }
769    }
770
771    impl StubAgentConnection {
772        /// Returns a model selector for this stub connection.
773        pub fn model_selector_impl(&self) -> Rc<dyn AgentModelSelector> {
774            Rc::new(StubModelSelector::new())
775        }
776    }
777}
778
779#[cfg(feature = "test-support")]
780pub use test_support::*;