gemini_memory_rs/runtime/
spec_binding.rs

1//! The [`MemoryBinding`] implementation — how a `SessionSpec`'s `memory`
2//! section reaches the real engine.
3//!
4//! The spec side (in `gemini-adk-fluent-rs`) is pure data: slots, and
5//! `remember` effects. This module is the other half of that seam: it hands
6//! the declaration to [`LiveMemoryExt::with_memory_slots`] (tools, ingestion,
7//! reconciliation, slot projection) and routes `remember` effects through
8//! [`MemorySession::apply_explicit_command`] — the same path the
9//! `manage_memory` tool takes, so a spec-authored remember and a user-asked
10//! remember are indistinguishable downstream.
11
12use std::sync::Arc;
13
14use gemini_adk_fluent_rs::live::Live;
15use gemini_adk_fluent_rs::spec::{MemoryBinding, MemorySpec};
16
17use super::live::LiveMemoryExt;
18use super::turn_extractor::MemorySlot;
19use crate::core::MutationIntent;
20use crate::engine::MemorySession;
21
22/// [`MemoryBinding`] over a [`MemorySession`].
23///
24/// ```no_run
25/// # use std::sync::Arc;
26/// # use gemini_memory_rs::prelude::*;
27/// # use gemini_memory_rs::runtime::SessionMemoryBinding;
28/// # use gemini_adk_fluent_rs::spec::SpecResources;
29/// let engine = MemoryEngine::in_memory(UserId::new("usr_1"));
30/// let session = Arc::new(engine.begin_session(SessionId::new("ses_1")));
31/// let resources = SpecResources {
32///     memory: Some(Arc::new(SessionMemoryBinding::new(session))),
33///     ..Default::default()
34/// };
35/// ```
36pub struct SessionMemoryBinding {
37    session: Arc<MemorySession>,
38}
39
40impl SessionMemoryBinding {
41    /// Bind a memory session for spec-driven installation.
42    pub fn new(session: Arc<MemorySession>) -> Self {
43        Self { session }
44    }
45
46    /// The underlying session.
47    pub fn session(&self) -> &Arc<MemorySession> {
48        &self.session
49    }
50}
51
52impl MemoryBinding for SessionMemoryBinding {
53    fn install(&self, live: Live, memory: &MemorySpec) -> Live {
54        let slots: Vec<MemorySlot> = memory
55            .slots
56            .iter()
57            .filter_map(|s| {
58                // Spec validation already rejects `derived:` targets; any slot
59                // the constructor still refuses is dropped rather than
60                // panicking a connect.
61                MemorySlot::try_new(&s.predicate, &s.to).ok()
62            })
63            .collect();
64        live.with_memory_slots(self.session.clone(), slots)
65    }
66
67    fn remember(&self, note: String) {
68        let session = self.session.clone();
69        tokio::spawn(async move {
70            let turn = session.current_turn();
71            let _ = session
72                .apply_explicit_command(MutationIntent::Remember, &note, turn)
73                .await;
74        });
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use crate::core::{SessionId, TurnId, UserId};
82    use crate::engine::MemoryEngine;
83    use gemini_adk_fluent_rs::spec::MemorySlotSpec;
84
85    fn session() -> Arc<MemorySession> {
86        let engine = MemoryEngine::in_memory(UserId::new("usr_1"));
87        let session = Arc::new(engine.begin_session(SessionId::new("ses_1")));
88        session.begin_turn(TurnId(1));
89        session
90    }
91
92    #[tokio::test]
93    async fn remember_commits_through_the_explicit_command_path() {
94        let session = session();
95        let binding = SessionMemoryBinding::new(session.clone());
96        binding.remember("The caller prefers evening appointments".into());
97        // The write is fire-and-forget; give the spawned task a beat.
98        for _ in 0..50 {
99            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
100            if !session.known_statements().is_empty() {
101                break;
102            }
103        }
104        assert!(
105            session
106                .known_statements()
107                .iter()
108                .any(|s| s.contains("evening")),
109            "statements: {:?}",
110            session.known_statements()
111        );
112    }
113
114    #[tokio::test]
115    async fn install_wires_slots_onto_the_builder() {
116        let binding = SessionMemoryBinding::new(session());
117        let memory = MemorySpec {
118            slots: vec![MemorySlotSpec {
119                predicate: "dietary_identity".into(),
120                to: "user:diet".into(),
121            }],
122        };
123        // Building without panicking is the contract here — the slot wiring
124        // itself is covered by the runtime's own tests.
125        let _ = binding.install(Live::builder(), &memory);
126    }
127}