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}