Skip to main content

synapto_interface/
context.rs

1#![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/// Unified registries container for the cognitive engine.
195#[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}