1use crate::agent::{ConversationState, ReplayBound};
2use crate::context::{Checkpoint, ContextAssembler, ContextMessage, ContextPolicy, LastN};
3use crate::error::LaserError;
4use crate::laser::Laser;
5use crate::memory::{
6 ConsolidationReport, Feedback, MemoryBackend, MemoryHandle, MemoryId, MemoryScope,
7 RecallBuilder, RememberBuilder,
8};
9use crate::provenance::{AgentTopic, Provenance};
10use crate::snapshot::SnapshotStore;
11use crate::types::ConversationId;
12
13impl Laser {
14 pub fn context(&self, conversation: ConversationId) -> ContextScope {
19 ContextScope {
20 laser: self.clone(),
21 conversation,
22 }
23 }
24}
25
26#[derive(Clone)]
29pub struct ContextScope {
30 laser: Laser,
31 conversation: ConversationId,
32}
33
34impl ContextScope {
35 pub async fn append(
39 &self,
40 topic: AgentTopic<'_>,
41 payload: impl Into<Vec<u8>>,
42 ) -> Result<(), LaserError> {
43 let provenance = Provenance::builder()
44 .conversation_id(self.conversation)
45 .build();
46 self.laser.send_agent(topic, payload, &provenance).await
47 }
48
49 pub async fn fetch(
56 &self,
57 topics: Vec<AgentTopic<'static>>,
58 n: usize,
59 token_budget: Option<usize>,
60 ) -> Result<Vec<ContextMessage>, LaserError> {
61 self.fetch_with(topics, bounded_policy(n, token_budget))
62 .await
63 }
64
65 pub async fn fetch_with(
68 &self,
69 topics: Vec<AgentTopic<'static>>,
70 policy: Box<dyn ContextPolicy>,
71 ) -> Result<Vec<ContextMessage>, LaserError> {
72 ContextAssembler::builder()
73 .conversation_id(self.conversation)
74 .topics(topics)
75 .policy(policy)
76 .build()
77 .assemble(&self.laser)
78 .await
79 }
80
81 pub async fn block(
86 &self,
87 topics: Vec<AgentTopic<'static>>,
88 n: usize,
89 token_budget: Option<usize>,
90 ) -> Result<String, LaserError> {
91 let messages = self.fetch(topics, n, token_budget).await?;
92 Ok(messages
93 .iter()
94 .map(|message| String::from_utf8_lossy(&message.payload))
95 .collect::<Vec<_>>()
96 .join("\n"))
97 }
98
99 pub async fn state<S, F>(
103 &self,
104 topics: Vec<AgentTopic<'static>>,
105 bound: ReplayBound,
106 init: S,
107 fold: F,
108 ) -> Result<S, LaserError>
109 where
110 F: FnMut(S, &ContextMessage) -> S,
111 {
112 ConversationState::load(&self.laser, self.conversation, topics, bound, init, fold).await
113 }
114
115 pub async fn state_with<Store, S, F>(
119 &self,
120 store: &Store,
121 topics: Vec<AgentTopic<'static>>,
122 init: S,
123 fold: F,
124 ) -> Result<S, LaserError>
125 where
126 Store: SnapshotStore + Sync,
127 S: serde::de::DeserializeOwned,
128 F: FnMut(S, &ContextMessage) -> S,
129 {
130 ConversationState::load_with(&self.laser, store, self.conversation, topics, init, fold)
131 .await
132 }
133
134 pub fn memory(&self, namespace: impl Into<String>) -> ScopedMemory {
141 self.memory_with(namespace, MemoryBackend::Auto)
142 }
143
144 pub fn memory_with(
146 &self,
147 namespace: impl Into<String>,
148 backend: MemoryBackend,
149 ) -> ScopedMemory {
150 ScopedMemory {
151 handle: self.laser.memory_with(namespace, backend),
152 conversation: self.conversation,
153 origin: None,
154 producer: None,
155 }
156 }
157
158 #[cfg(feature = "graph")]
167 pub fn graph(&self, name: impl Into<String>) -> crate::graph::GraphHandle<'_> {
168 self.laser.graph(name)
169 }
170
171 pub async fn checkpoint(
176 &self,
177 topics: &[AgentTopic<'static>],
178 ) -> Result<Checkpoint, LaserError> {
179 crate::context::checkpoint(&self.laser, topics).await
180 }
181
182 pub fn conversation(&self) -> ConversationId {
184 self.conversation
185 }
186
187 pub fn laser(&self) -> &Laser {
189 &self.laser
190 }
191}
192
193fn bounded_policy(n: usize, token_budget: Option<usize>) -> Box<dyn ContextPolicy> {
195 match token_budget {
196 Some(max_tokens) => Box::new(crate::context::Chain(vec![
197 Box::new(LastN(n)),
198 Box::new(crate::context::TokenBudget::new(max_tokens)),
199 ])),
200 None => Box::new(LastN(n)),
201 }
202}
203
204pub struct ScopedMemory {
210 handle: MemoryHandle,
211 conversation: ConversationId,
212 origin: Option<laser_wire::graph::SourceRef>,
213 producer: Option<laser_wire::graph::ProducerInfo>,
214}
215
216impl ScopedMemory {
217 pub fn recall(&self) -> RecallBuilder<'_> {
221 self.handle.recall(self.conversation)
222 }
223
224 pub fn remember(&self, payload: impl Into<Vec<u8>>) -> RememberBuilder<'_> {
227 let mut builder = self.handle.remember(payload).scope(self.conversation);
228 if let Some(origin) = &self.origin {
229 builder = builder.origin(origin.clone());
230 }
231 if let Some(producer) = &self.producer {
232 builder = builder.producer(producer.clone());
233 }
234 builder
235 }
236
237 #[must_use]
239 pub fn with_lineage(
240 mut self,
241 origin: Option<laser_wire::graph::SourceRef>,
242 producer: Option<laser_wire::graph::ProducerInfo>,
243 ) -> Self {
244 self.origin = origin;
245 self.producer = producer;
246 self
247 }
248
249 pub async fn block(&self, token_budget: Option<usize>) -> Result<String, LaserError> {
252 self.handle.context(self.conversation, token_budget).await
253 }
254
255 pub fn search(&self, query: impl Into<String>) -> RecallBuilder<'_> {
261 self.recall().keyword(query)
262 }
263
264 pub async fn consolidate(&self, max_items: usize) -> Result<ConsolidationReport, LaserError> {
267 self.handle.consolidate(&self.scope(), max_items).await
268 }
269
270 pub async fn consolidate_with(
273 &self,
274 max_items: usize,
275 summarizer: impl crate::memory::Summarizer + Sync,
276 prune_summarized: bool,
277 ) -> Result<ConsolidationReport, LaserError> {
278 self.handle
279 .consolidate_with(&self.scope(), max_items, summarizer, prune_summarized)
280 .await
281 }
282
283 pub async fn forget(&self, id: MemoryId) -> Result<(), LaserError> {
286 self.handle.forget(&self.scope(), id).await
287 }
288
289 pub async fn improve(&self, feedback: Feedback) -> Result<MemoryId, LaserError> {
292 self.handle.improve(&self.scope(), feedback).await
293 }
294
295 pub fn handle(&self) -> &MemoryHandle {
298 &self.handle
299 }
300
301 pub fn conversation(&self) -> ConversationId {
303 self.conversation
304 }
305
306 pub fn origin(&self) -> Option<&laser_wire::graph::SourceRef> {
308 self.origin.as_ref()
309 }
310
311 pub fn producer(&self) -> Option<&laser_wire::graph::ProducerInfo> {
313 self.producer.as_ref()
314 }
315
316 fn scope(&self) -> MemoryScope {
317 MemoryScope::builder()
318 .conversation(self.conversation)
319 .build()
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326 use crate::memory::Embedder;
327
328 struct UnitEmbedder;
329
330 impl Embedder for UnitEmbedder {
331 async fn embed(&self, _text: &str) -> Result<Vec<f32>, LaserError> {
332 Ok(vec![1.0])
333 }
334 }
335
336 #[tokio::test]
337 async fn given_an_item_of_another_conversation_when_forgotten_through_a_scope_then_should_keep_it()
338 {
339 let owner = ConversationId::new();
340 let handle = MemoryHandle::vector(UnitEmbedder);
341 let id = handle
342 .remember("a fact")
343 .scope(owner)
344 .send()
345 .await
346 .expect("remember");
347 let elsewhere = ScopedMemory {
348 handle,
349 conversation: ConversationId::new(),
350 origin: None,
351 producer: None,
352 };
353 elsewhere.forget(id).await.expect("forget by id");
354 let left = elsewhere
355 .handle()
356 .recall(owner)
357 .fetch()
358 .await
359 .expect("recall");
360 assert_eq!(left.len(), 1, "another conversation cannot forget the item");
361 }
362
363 struct JoinSummarizer;
364
365 impl crate::memory::Summarizer for JoinSummarizer {
366 async fn summarize(&self, bodies: Vec<Vec<u8>>) -> Result<Vec<u8>, LaserError> {
367 Ok(bodies.concat())
368 }
369 }
370
371 fn scoped(handle: MemoryHandle, conversation: ConversationId) -> ScopedMemory {
372 ScopedMemory {
373 handle,
374 conversation,
375 origin: None,
376 producer: None,
377 }
378 }
379
380 #[tokio::test]
381 async fn given_two_conversations_when_recalled_without_one_then_should_return_both() {
382 let handle = MemoryHandle::vector(UnitEmbedder);
383 for conversation in [ConversationId::new(), ConversationId::new()] {
384 handle
385 .remember("a fact")
386 .scope(conversation)
387 .send()
388 .await
389 .expect("remember");
390 }
391 let everywhere = handle
392 .recall(None)
393 .await
394 .expect("recall every conversation");
395 assert_eq!(everywhere.len(), 2);
396 }
397
398 #[tokio::test]
399 async fn given_a_search_when_limited_then_should_cap_the_items() {
400 let conversation = ConversationId::new();
401 let memory = scoped(MemoryHandle::vector(UnitEmbedder), conversation);
402 for body in ["deploy one", "deploy two", "deploy three"] {
403 memory.remember(body).send().await.expect("remember");
404 }
405 assert_eq!(memory.search("deploy").await.expect("search").len(), 3);
406 let capped = memory.search("deploy").limit(1).await.expect("search");
407 assert_eq!(capped.len(), 1);
408 }
409
410 #[tokio::test]
411 async fn given_message_turns_when_consolidated_with_a_summarizer_then_should_replace_them() {
412 let conversation = ConversationId::new();
413 let memory = scoped(MemoryHandle::vector(UnitEmbedder), conversation);
414 for body in ["one ", "two"] {
415 memory
416 .remember(body)
417 .kind(crate::memory::MemoryKind::Message)
418 .send()
419 .await
420 .expect("remember");
421 }
422 let report = memory
423 .consolidate_with(10, JoinSummarizer, true)
424 .await
425 .expect("consolidate");
426 assert_eq!(report.summarized, 2);
427 assert_eq!(report.pruned, 2);
428 }
429}