synapto_interface/
context.rs1#![doc = include_str!("context.md")]
2
3use crate::llm::LLMSafe;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum TemporalScope {
7 Historical,
8 Current,
9 Prospective,
10}
11
12#[derive(Clone, Debug, serde :: Serialize, serde :: Deserialize)]
13pub struct ContextInteraction {
14 pub peer_input: Option<String>,
15 pub cognitive_reasoning: Option<String>,
16 pub cognitive_output: Option<String>,
17}
18
19#[derive(Clone, Debug, serde :: Serialize, serde :: Deserialize, Default)]
20pub struct ContextRequest {
21 #[doc = " The sliding window of recent conversational flow."]
22 #[doc = " Used by plugins to perform Associative RAG."]
23 #[doc = " An empty list implies a request for the unfiltered, baseline state."]
24 pub recent_interactions: Vec<ContextInteraction>,
25 pub initial_run: bool,
26}
27
28#[async_trait::async_trait]
29pub trait ContextProvider: Send + Sync + 'static {
30 type Context: schemars::JsonSchema + serde::Serialize + LLMSafe + Send + Sync + 'static;
31 #[doc = " Declarative compile-time semantic key (e.g., \"state\", \"active_tasks\")"]
32 const NAME: &'static str;
33 #[doc = " The dimension this context belongs to"]
34 const SCOPE: TemporalScope;
35 #[doc = " Provide the JSON-serializable context view, filtered associatively via ContextRequest"]
36 async fn context(&self, request: &ContextRequest) -> Result<Self::Context, String>;
37 #[doc = " Decentralized Wakeup Signal:"]
38 #[doc = " Returns a receiver that signals when this specific context mutates."]
39 fn subscribe(&self) -> Option<tokio::sync::watch::Receiver<()>> {
40 None
41 }
42}
43
44#[async_trait::async_trait]
45pub trait ErasedContextProvider: Send + Sync + 'static {
46 fn name(&self) -> &'static str;
47 fn scope(&self) -> TemporalScope;
48 fn schema(&self) -> schemars::Schema;
49 async fn erased_context(&self, request: &ContextRequest) -> Result<serde_json::Value, String>;
50 fn subscribe(&self) -> Option<tokio::sync::watch::Receiver<()>>;
51}
52
53#[async_trait::async_trait]
54impl<T> ErasedContextProvider for T
55where
56 T: ContextProvider,
57{
58 fn name(&self) -> &'static str {
59 <T as ContextProvider>::NAME
60 }
61 fn scope(&self) -> TemporalScope {
62 <T as ContextProvider>::SCOPE
63 }
64 fn schema(&self) -> schemars::Schema {
65 schemars::schema_for!(<T as ContextProvider>::Context)
66 }
67 async fn erased_context(&self, request: &ContextRequest) -> Result<serde_json::Value, String> {
68 let view = <T as ContextProvider>::context(self, request).await?;
69 serde_json::to_value(view).map_err(|e| e.to_string())
70 }
71 fn subscribe(&self) -> Option<tokio::sync::watch::Receiver<()>> {
72 <T as ContextProvider>::subscribe(self)
73 }
74}
75
76pub struct ContextRegistryBuilder {
77 providers: std::sync::RwLock<Vec<std::sync::Arc<dyn ErasedContextProvider>>>,
78 change_tx: tokio::sync::watch::Sender<()>,
79 change_rx: tokio::sync::watch::Receiver<()>,
80}
81
82impl Default for ContextRegistryBuilder {
83 fn default() -> Self {
84 let (change_tx, change_rx) = tokio::sync::watch::channel(());
85 Self {
86 providers: std::sync::RwLock::new(Vec::new()),
87 change_tx,
88 change_rx,
89 }
90 }
91}
92
93impl ContextRegistryBuilder {
94 pub async fn gather_contexts(
95 &self,
96 request: &ContextRequest,
97 ) -> std::collections::BTreeMap<String, serde_json::Value> {
98 let providers: Vec<_> = self
99 .providers
100 .read()
101 .unwrap_or_else(|e| panic!("Providers lock poisoned: {:?}", e))
102 .clone();
103
104 let futures = providers.into_iter().map(|provider| {
105 let request = request.clone();
106 async move {
107 let name = provider.name().to_string();
108 let res = provider.erased_context(&request).await;
109 (name, res)
110 }
111 });
112
113 let results = futures::future::join_all(futures).await;
114
115 let mut contexts = std::collections::BTreeMap::new();
116 for (name, res) in results {
117 if let Ok(val) = res {
118 contexts.insert(name, val);
119 }
120 }
121 contexts
122 }
123
124 pub fn register<T>(&self, provider: T)
125 where
126 T: ErasedContextProvider + 'static,
127 {
128 let provider_arc: std::sync::Arc<dyn ErasedContextProvider> = std::sync::Arc::new(provider);
129 self.register_erased(provider_arc);
130 }
131
132 pub fn is_empty(&self) -> bool {
133 self.providers
134 .read()
135 .unwrap_or_else(|e| panic!("Providers lock poisoned: {:?}", e))
136 .is_empty()
137 }
138
139 pub fn register_erased(&self, provider: std::sync::Arc<dyn ErasedContextProvider>) {
140 self.providers
141 .write()
142 .unwrap_or_else(|e| panic!("Failed to acquire write lock on providers: {:?}", e))
143 .push(provider.clone());
144 if let Some(mut sub_rx) = provider.subscribe() {
145 let change_tx = self.change_tx.clone();
146 tokio::spawn(async move {
147 while sub_rx.changed().await.is_ok() {
148 change_tx
149 .send(())
150 .inspect_err(|e| tracing::error!("{}", e))
151 .ok();
152 }
153 });
154 }
155 }
156 pub fn subscribe(&self) -> tokio::sync::watch::Receiver<()> {
157 self.change_rx.clone()
158 }
159}
160
161pub trait IntoContextProvider {
162 fn into_erased_context_provider(self) -> std::sync::Arc<dyn ErasedContextProvider>;
163}
164
165impl<T: ContextProvider> IntoContextProvider for T {
166 fn into_erased_context_provider(self) -> std::sync::Arc<dyn ErasedContextProvider> {
167 std::sync::Arc::new(self)
168 }
169}
170
171impl<T: ContextProvider> IntoContextProvider for std::sync::Arc<T> {
172 fn into_erased_context_provider(self) -> std::sync::Arc<dyn ErasedContextProvider> {
173 self
174 }
175}
176
177#[derive(Default)]
178pub struct ContextRegistries {
179 pub historical: ContextRegistryBuilder,
180 pub current: ContextRegistryBuilder,
181 pub prospective: ContextRegistryBuilder,
182}
183
184impl ContextRegistries {
185 pub fn subscribe(&self, scope: TemporalScope) -> tokio::sync::watch::Receiver<()> {
186 match scope {
187 TemporalScope::Historical => self.historical.subscribe(),
188 TemporalScope::Current => self.current.subscribe(),
189 TemporalScope::Prospective => self.prospective.subscribe(),
190 }
191 }
192}
193
194#[derive(Clone, Default)]
196pub struct EngineRegistries {
197 pub context: std::sync::Arc<ContextRegistries>,
198 pub tools: std::sync::Arc<crate::tool::ToolRegistryBuilder>,
199 pub commands: std::sync::Arc<crate::command::CommandRegistryBuilder>,
200}
201
202impl std::fmt::Debug for EngineRegistries {
203 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204 f.debug_struct("EngineRegistries").finish_non_exhaustive()
205 }
206}