Skip to main content

behest_runtime/
extensions.rs

1//! [`Extensions`]: the composable, hot-pluggable facade over every
2//! pluggable runtime element.
3//!
4//! Every category of plug-in — chat providers, embedding providers, tools,
5//! context adapters, session stores, execution stores, embedding stores,
6//! artifact stores, run stores, event publishers, session data stores,
7//! runtime event stores, snapshot stores, and RAG adapters — is exposed
8//! here as an [`ExtensionPoint<T>`]. Operators compose a runtime by
9//! registering implementations by name, and can hot-swap any
10//! registered instance at runtime.
11//!
12//! # Construction
13//!
14//! [`Extensions::default`] returns a fresh, empty facade suitable for
15//! greenfield construction. Existing `AgentRuntime` instances
16//! expose their built-in components via
17//! [`AgentRuntime::extensions`](super::agent::AgentRuntime::extensions),
18//! which materializes an `Extensions` from the runtime's internal
19//! registries on demand.
20//!
21//! # Threading
22//!
23//! Every field is an [`ExtensionPoint<T>`], which is internally
24//! `Arc`-backed and uses `RwLock` for storage. Cloning `Extensions`
25//! is cheap and all clones observe the same set of registered entries.
26//!
27//! # Examples
28//!
29//! ```rust
30//! use std::sync::Arc;
31//! use behest_provider::{ChatProvider, ProviderId, ChatRequest, ChatResponse, ProviderResult};
32//! use behest_runtime::extensions::Extensions;
33//!
34//! let exts = Extensions::default();
35//! let _ = exts;
36//! ```
37
38#![allow(clippy::pedantic)]
39
40use super::extension::ExtensionPoint;
41use super::store::RunStore;
42use behest_provider::{ChatProvider, EmbeddingProvider};
43use behest_store::{ArtifactStore, EmbeddingStore, ExecutionStore, SessionStore};
44
45#[cfg(feature = "queue")]
46use crate::event_publisher::EventPublisher;
47
48use super::event_store::RuntimeEventStore;
49use super::invocation::SessionDataStore;
50use super::snapshot::SnapshotStore;
51
52/// The composable, hot-pluggable facade over every pluggable runtime
53/// element.
54///
55/// All fields are public so callers can register, replace, and inspect
56/// every category of plug-in directly. Cloning is cheap (`Arc`-backed
57/// internally).
58#[derive(Clone, Default)]
59pub struct Extensions {
60    /// Chat provider implementations, keyed by user-assigned name.
61    pub chat_providers: ExtensionPoint<dyn ChatProvider>,
62    /// Embedding provider implementations, keyed by user-assigned name.
63    pub embedding_providers: ExtensionPoint<dyn EmbeddingProvider>,
64    /// Tool implementations, keyed by user-assigned name.
65    pub tools: ExtensionPoint<dyn behest_tool::Tool>,
66    /// Context adapter implementations, keyed by user-assigned name.
67    pub context_adapters: ExtensionPoint<dyn behest_context::ContextAdapter>,
68    /// Session store implementations, keyed by user-assigned name.
69    pub session_stores: ExtensionPoint<dyn SessionStore>,
70    /// Execution store implementations, keyed by user-assigned name.
71    pub execution_stores: ExtensionPoint<dyn ExecutionStore>,
72    /// Embedding store implementations, keyed by user-assigned name.
73    pub embedding_stores: ExtensionPoint<dyn EmbeddingStore>,
74    /// Artifact store implementations, keyed by user-assigned name.
75    pub artifact_stores: ExtensionPoint<dyn ArtifactStore>,
76    /// Run store implementations, keyed by user-assigned name.
77    pub run_stores: ExtensionPoint<dyn RunStore>,
78    /// Event publisher implementations, keyed by user-assigned name.
79    #[cfg(feature = "queue")]
80    pub event_publishers: ExtensionPoint<dyn EventPublisher>,
81    /// Session data store implementations, keyed by user-assigned name.
82    pub session_data_stores: ExtensionPoint<dyn SessionDataStore>,
83    /// Runtime event store implementations, keyed by user-assigned name.
84    pub runtime_event_stores: ExtensionPoint<dyn RuntimeEventStore>,
85    /// Snapshot store implementations, keyed by user-assigned name.
86    pub snapshot_stores: ExtensionPoint<dyn SnapshotStore>,
87}
88
89impl Extensions {
90    /// Construct a fresh, empty `Extensions`.
91    #[must_use]
92    pub fn new() -> Self {
93        Self::default()
94    }
95
96    /// Number of distinct extension-point categories that have at least
97    /// one registered entry.
98    #[must_use]
99    pub fn populated_categories(&self) -> usize {
100        macro_rules! count {
101            ($n:ident, $($field:ident),+ $(,)?) => {
102                $( if !self.$field.is_empty() { $n += 1; } )+
103            };
104        }
105        let mut n = 0;
106        count!(
107            n,
108            chat_providers,
109            embedding_providers,
110            tools,
111            context_adapters
112        );
113        count!(
114            n,
115            session_stores,
116            execution_stores,
117            embedding_stores,
118            artifact_stores,
119            run_stores
120        );
121        #[cfg(feature = "queue")]
122        if !self.event_publishers.is_empty() {
123            n += 1;
124        }
125        count!(
126            n,
127            session_data_stores,
128            runtime_event_stores,
129            snapshot_stores
130        );
131        n
132    }
133}
134
135impl std::fmt::Debug for Extensions {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        f.debug_struct("Extensions")
138            .field("chat_providers", &self.chat_providers.names())
139            .field("embedding_providers", &self.embedding_providers.names())
140            .field("tools", &self.tools.names())
141            .field("context_adapters", &self.context_adapters.names())
142            .field("session_stores", &self.session_stores.names())
143            .field("execution_stores", &self.execution_stores.names())
144            .field("embedding_stores", &self.embedding_stores.names())
145            .field("artifact_stores", &self.artifact_stores.names())
146            .field("run_stores", &self.run_stores.names())
147            .field("session_data_stores", &self.session_data_stores.names())
148            .field("runtime_event_stores", &self.runtime_event_stores.names())
149            .field("snapshot_stores", &self.snapshot_stores.names())
150            .finish()
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn empty_extensions_reports_zero_populated() {
160        let exts = Extensions::new();
161        assert_eq!(exts.populated_categories(), 0);
162    }
163
164    #[test]
165    fn clone_shares_state() {
166        let exts = Extensions::new();
167        let exts2 = exts.clone();
168        // Both clones observe the same underlying state.
169        assert_eq!(exts.populated_categories(), exts2.populated_categories());
170    }
171}