Skip to main content

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 one [`MemorySession`] and authenticated memory subject.
23///
24/// Task activations share this session's backend, while each declares its own
25/// projected slots. Use one binding per logical Live session. Accepted task
26/// effects and contextual memory tools serialize with awaited reconciliation;
27/// task cancellation does not undo an effect that already started.
28///
29/// ```no_run
30/// # use std::sync::Arc;
31/// # use gemini_memory_rs::prelude::*;
32/// # use gemini_memory_rs::runtime::SessionMemoryBinding;
33/// # use gemini_adk_fluent_rs::spec::SpecResources;
34/// let engine = MemoryEngine::in_memory(UserId::new("usr_1"));
35/// let session = Arc::new(engine.begin_session(SessionId::new("ses_1")));
36/// let resources = SpecResources {
37///     memory: Some(Arc::new(SessionMemoryBinding::new(session))),
38///     ..Default::default()
39/// };
40/// ```
41pub struct SessionMemoryBinding {
42    session: Arc<MemorySession>,
43    task_session: Arc<super::task_binding::TaskMemorySession>,
44}
45
46impl SessionMemoryBinding {
47    /// Bind a memory session for spec-driven installation.
48    pub fn new(session: Arc<MemorySession>) -> Self {
49        Self {
50            task_session: super::task_binding::TaskMemorySession::new(session.clone()),
51            session,
52        }
53    }
54
55    /// The underlying session.
56    pub fn session(&self) -> &Arc<MemorySession> {
57        &self.session
58    }
59}
60
61impl MemoryBinding for SessionMemoryBinding {
62    fn install(&self, live: Live, memory: &MemorySpec) -> Live {
63        let slots: Vec<MemorySlot> = memory
64            .slots
65            .iter()
66            .filter_map(|s| {
67                // Spec validation already rejects `derived:` targets; any slot
68                // the constructor still refuses is dropped rather than
69                // panicking a connect.
70                MemorySlot::try_new(&s.predicate, &s.to).ok()
71            })
72            .collect();
73        live.with_memory_slots(self.session.clone(), slots)
74    }
75
76    fn remember(&self, note: String) {
77        let session = self.session.clone();
78        tokio::spawn(async move {
79            let turn = session.current_turn();
80            let _ = session
81                .apply_explicit_command(MutationIntent::Remember, &note, turn)
82                .await;
83        });
84    }
85
86    fn task_memory(
87        &self,
88        memory: &MemorySpec,
89    ) -> Result<Arc<dyn gemini_adk_rs::tasks::TaskMemoryService>, String> {
90        let slots = memory
91            .slots
92            .iter()
93            .map(|slot| {
94                MemorySlot::try_new(&slot.predicate, &slot.to).map_err(|error| error.to_string())
95            })
96            .collect::<Result<Vec<_>, _>>()?;
97        Ok(self.task_session.view(slots))
98    }
99
100    fn task_tools(&self) -> Vec<Arc<dyn gemini_adk_rs::tool::ToolFunction>> {
101        self.task_session.tools()
102    }
103
104    fn install_task_lifecycle(&self, live: Live) -> Live {
105        let shared = self.task_session.clone();
106        live.on_teardown(move || {
107            let shared = shared.clone();
108            async move {
109                if let Err(error) = shared.finish().await {
110                    tracing::error!(%error, "task memory reconciliation failed");
111                }
112            }
113        })
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use crate::core::{SessionId, TurnId, UserId};
121    use crate::engine::MemoryEngine;
122    use gemini_adk_fluent_rs::spec::MemorySlotSpec;
123
124    fn session() -> Arc<MemorySession> {
125        let engine = MemoryEngine::in_memory(UserId::new("usr_1"));
126        let session = Arc::new(engine.begin_session(SessionId::new("ses_1")));
127        session.begin_turn(TurnId(1));
128        session
129    }
130
131    #[tokio::test]
132    async fn remember_commits_through_the_explicit_command_path() {
133        let session = session();
134        let binding = SessionMemoryBinding::new(session.clone());
135        binding.remember("The caller prefers evening appointments".into());
136        // The write is fire-and-forget; give the spawned task a beat.
137        for _ in 0..50 {
138            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
139            if !session.known_statements().is_empty() {
140                break;
141            }
142        }
143        assert!(
144            session
145                .known_statements()
146                .iter()
147                .any(|s| s.contains("evening")),
148            "statements: {:?}",
149            session.known_statements()
150        );
151    }
152
153    #[tokio::test]
154    async fn install_wires_slots_onto_the_builder() {
155        let binding = SessionMemoryBinding::new(session());
156        let memory = MemorySpec {
157            slots: vec![MemorySlotSpec {
158                predicate: "dietary_identity".into(),
159                to: "user:diet".into(),
160            }],
161        };
162        // Building without panicking is the contract here — the slot wiring
163        // itself is covered by the runtime's own tests.
164        let _ = binding.install(Live::builder(), &memory);
165    }
166}