1use crate::agent::Laser;
2use crate::error::LaserError;
3use crate::provenance::{AgentTopic, Provenance};
4use crate::types::{AgentId, ConversationId, IdError};
5use iggy::prelude::*;
6use laser_wire::graph::SourceRef;
7use serde::{Deserialize, Serialize};
8use std::collections::HashSet;
9use std::fmt::{self, Display, Formatter};
10use std::str::FromStr;
11use tokio::sync::Mutex;
12use ulid::Ulid;
13
14const READ_BATCH: u32 = 1000;
16
17const CONSOLIDATION_WINDOW: usize = 10_000;
21
22pub const DEFAULT_MEMORY_TOPIC_TTL: std::time::Duration =
29 std::time::Duration::from_secs(30 * 24 * 60 * 60);
30
31#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum MemoryKind {
36 #[default]
38 Fact,
39 Message,
41 Summary,
43 Entity,
45 Feedback,
47 Procedure,
50}
51
52impl MemoryKind {
53 pub const fn code(self) -> u8 {
56 match self {
57 MemoryKind::Fact => 1,
58 MemoryKind::Message => 2,
59 MemoryKind::Summary => 3,
60 MemoryKind::Entity => 4,
61 MemoryKind::Feedback => 5,
62 MemoryKind::Procedure => 6,
63 }
64 }
65
66 pub fn from_word(word: &str) -> MemoryKind {
70 match word {
71 "message" => MemoryKind::Message,
72 "summary" => MemoryKind::Summary,
73 "entity" => MemoryKind::Entity,
74 "feedback" => MemoryKind::Feedback,
75 "procedure" => MemoryKind::Procedure,
76 _ => MemoryKind::Fact,
77 }
78 }
79
80 pub const fn class(self) -> MemoryClass {
85 match self {
86 MemoryKind::Message => MemoryClass::Episodic,
87 MemoryKind::Procedure => MemoryClass::Procedural,
88 MemoryKind::Fact | MemoryKind::Summary | MemoryKind::Entity | MemoryKind::Feedback => {
89 MemoryClass::Semantic
90 }
91 }
92 }
93}
94
95#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
99#[serde(rename_all = "snake_case")]
100pub enum MemoryClass {
101 Episodic,
103 Semantic,
105 Procedural,
107}
108
109#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
112#[serde(rename_all = "snake_case")]
113pub enum Lifetime {
114 #[default]
116 Session,
117 Durable,
119}
120
121#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
124#[serde(rename_all = "snake_case")]
125pub enum RecallStrategy {
126 #[default]
128 Auto,
129 Recent,
131 Semantic,
133 Keyword,
136 Graph,
138 Temporal,
140 Hybrid,
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
147pub struct MemoryId(Ulid);
148
149impl MemoryId {
150 pub fn new() -> Self {
152 Self(Ulid::generate())
153 }
154
155 pub fn as_u128(self) -> u128 {
157 self.0.0
158 }
159
160 pub fn from_u128(value: u128) -> Self {
162 Self(Ulid(value))
163 }
164
165 pub fn content(owner: &MemoryScope, kind: MemoryKind, body: &[u8]) -> Self {
173 let stream = owner.stream.as_deref().unwrap_or("").as_bytes();
174 let agent = owner.agent.as_ref().map(AgentId::as_str).unwrap_or("");
175 let user = owner.user.as_deref().unwrap_or("").as_bytes();
176 let application = owner.app.as_deref().unwrap_or("").as_bytes();
177 let kind = [kind.code()];
178 let segments: [&[u8]; 10] = [
181 stream,
182 &[0],
183 agent.as_bytes(),
184 &[0],
185 user,
186 &[0],
187 application,
188 &[0],
189 &kind,
190 body,
191 ];
192 Self::from_u128(laser_wire::hashing::content_id(&segments))
193 }
194}
195
196impl Serialize for MemoryId {
197 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
198 serializer.serialize_str(&self.to_string())
199 }
200}
201
202impl<'de> Deserialize<'de> for MemoryId {
203 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
204 let text = String::deserialize(deserializer)?;
205 text.parse().map_err(serde::de::Error::custom)
206 }
207}
208
209impl Default for MemoryId {
210 fn default() -> Self {
211 Self::new()
212 }
213}
214
215impl Display for MemoryId {
216 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
217 Display::fmt(&self.0, f)
218 }
219}
220
221impl FromStr for MemoryId {
222 type Err = IdError;
223
224 fn from_str(s: &str) -> Result<Self, Self::Err> {
225 Ulid::from_string(s)
226 .map(Self)
227 .map_err(|_| IdError::InvalidUlid(s.to_owned()))
228 }
229}
230
231#[derive(Debug, Clone, Default, bon::Builder)]
242pub struct MemoryScope {
243 pub stream: Option<String>,
245 pub user: Option<String>,
247 pub agent: Option<AgentId>,
249 pub conversation: Option<ConversationId>,
251 pub app: Option<String>,
253 #[builder(default)]
255 pub lifetime: Lifetime,
256 pub origin: Option<SourceRef>,
259 pub producer: Option<laser_wire::graph::ProducerInfo>,
262}
263
264#[derive(Debug, Clone)]
266pub struct MemoryItem {
267 pub id: MemoryId,
269 pub payload: Vec<u8>,
272 pub provenance: Provenance,
274 pub kind: MemoryKind,
276 pub score: Option<f32>,
279 pub signals: Vec<RecallSignal>,
283 pub source: Option<SourceRef>,
288 pub origin: Option<SourceRef>,
290 pub producer: Option<laser_wire::graph::ProducerInfo>,
292}
293
294impl MemoryItem {
295 pub fn text(&self) -> String {
297 String::from_utf8_lossy(&self.payload).into_owned()
298 }
299
300 pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, LaserError> {
302 serde_json::from_slice(&self.payload).map_err(|error| LaserError::Codec(error.to_string()))
303 }
304}
305
306#[cfg(test)]
307impl MemoryItem {
308 fn plain(id: MemoryId, payload: Vec<u8>, provenance: Provenance) -> Self {
309 Self {
310 id,
311 payload,
312 provenance,
313 kind: MemoryKind::Fact,
314 score: None,
315 signals: Vec::new(),
316 source: None,
317 origin: None,
318 producer: None,
319 }
320 }
321}
322
323#[derive(Debug, Clone, PartialEq)]
327pub struct RecallSignal {
328 pub strategy: RecallStrategy,
331 pub rank: usize,
333 pub score: Option<f32>,
335}
336
337pub fn to_context_block(items: &[MemoryItem], token_budget: Option<usize>) -> String {
344 let mut block = String::new();
345 let mut spent = 0usize;
346 let mut rendered = 0usize;
347 for item in items {
348 let text = String::from_utf8_lossy(&item.payload);
349 let cost = estimate_tokens(&text);
350 if let Some(budget) = token_budget
351 && rendered > 0
352 && spent + cost > budget
353 {
354 break;
355 }
356 if rendered > 0 {
357 block.push_str("\n\n");
358 }
359 block.push_str(&text);
360 spent += cost;
361 rendered += 1;
362 }
363 let omitted = items.len() - rendered;
364 if omitted > 0 {
365 if rendered > 0 {
366 block.push_str("\n\n");
367 }
368 block.push_str(&format!(
369 "[... {omitted} more recalled item(s) omitted ...]"
370 ));
371 }
372 block
373}
374
375fn estimate_tokens(text: &str) -> usize {
378 usize::try_from(laser_wire::agent::estimate_tokens(text.len())).unwrap_or(usize::MAX)
379}
380
381#[derive(Debug, Clone, bon::Builder)]
383pub struct MemoryQuery {
384 #[builder(default = 50)]
386 pub limit: usize,
387 pub token_budget: Option<usize>,
393 pub agent: Option<AgentId>,
394 pub semantic: Option<String>,
397 #[builder(default)]
401 pub strategy: RecallStrategy,
402}
403
404#[derive(Debug, Clone)]
408pub struct Feedback {
409 pub target: MemoryId,
411 pub weight: f32,
413 pub note: Option<String>,
415}
416
417impl Feedback {
418 pub fn new(target: MemoryId, weight: f32) -> Self {
420 Self {
421 target,
422 weight,
423 note: None,
424 }
425 }
426}
427
428#[trait_variant::make(Memory: Send)]
431pub trait LocalMemory {
432 async fn remember(&self, scope: &MemoryScope, payload: Vec<u8>)
435 -> Result<MemoryId, LaserError>;
436 fn append(
438 &self,
439 scope: &MemoryScope,
440 id: MemoryId,
441 kind: MemoryKind,
442 payload: Vec<u8>,
443 ) -> impl std::future::Future<Output = Result<MemoryId, LaserError>>
444 where
445 Self: Sync,
446 {
447 async move {
448 let _ = (id, kind);
449 self.remember(scope, payload).await
450 }
451 }
452 async fn recall(
453 &self,
454 scope: &MemoryScope,
455 query: &MemoryQuery,
456 ) -> Result<Vec<MemoryItem>, LaserError>;
457 async fn improve(
460 &self,
461 scope: &MemoryScope,
462 feedback: Feedback,
463 ) -> Result<MemoryId, LaserError>;
464 async fn forget(&self, scope: &MemoryScope, id: MemoryId) -> Result<(), LaserError>;
465}
466
467pub trait DynMemory: Send + Sync {
474 fn remember<'a>(
476 &'a self,
477 scope: &'a MemoryScope,
478 payload: Vec<u8>,
479 ) -> std::pin::Pin<
480 Box<dyn std::future::Future<Output = Result<MemoryId, LaserError>> + Send + 'a>,
481 >;
482 fn append<'a>(
484 &'a self,
485 scope: &'a MemoryScope,
486 id: MemoryId,
487 kind: MemoryKind,
488 payload: Vec<u8>,
489 ) -> std::pin::Pin<
490 Box<dyn std::future::Future<Output = Result<MemoryId, LaserError>> + Send + 'a>,
491 > {
492 let _ = (id, kind);
493 self.remember(scope, payload)
494 }
495 fn recall<'a>(
497 &'a self,
498 scope: &'a MemoryScope,
499 query: &'a MemoryQuery,
500 ) -> std::pin::Pin<
501 Box<dyn std::future::Future<Output = Result<Vec<MemoryItem>, LaserError>> + Send + 'a>,
502 >;
503 fn improve<'a>(
505 &'a self,
506 scope: &'a MemoryScope,
507 feedback: Feedback,
508 ) -> std::pin::Pin<
509 Box<dyn std::future::Future<Output = Result<MemoryId, LaserError>> + Send + 'a>,
510 >;
511 fn forget<'a>(
513 &'a self,
514 scope: &'a MemoryScope,
515 id: MemoryId,
516 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), LaserError>> + Send + 'a>>;
517}
518
519impl<M: Memory + Send + Sync> DynMemory for M {
520 fn remember<'a>(
521 &'a self,
522 scope: &'a MemoryScope,
523 payload: Vec<u8>,
524 ) -> std::pin::Pin<
525 Box<dyn std::future::Future<Output = Result<MemoryId, LaserError>> + Send + 'a>,
526 > {
527 Box::pin(Memory::remember(self, scope, payload))
528 }
529
530 fn append<'a>(
531 &'a self,
532 scope: &'a MemoryScope,
533 id: MemoryId,
534 kind: MemoryKind,
535 payload: Vec<u8>,
536 ) -> std::pin::Pin<
537 Box<dyn std::future::Future<Output = Result<MemoryId, LaserError>> + Send + 'a>,
538 > {
539 Box::pin(Memory::append(self, scope, id, kind, payload))
540 }
541
542 fn recall<'a>(
543 &'a self,
544 scope: &'a MemoryScope,
545 query: &'a MemoryQuery,
546 ) -> std::pin::Pin<
547 Box<dyn std::future::Future<Output = Result<Vec<MemoryItem>, LaserError>> + Send + 'a>,
548 > {
549 Box::pin(Memory::recall(self, scope, query))
550 }
551
552 fn improve<'a>(
553 &'a self,
554 scope: &'a MemoryScope,
555 feedback: Feedback,
556 ) -> std::pin::Pin<
557 Box<dyn std::future::Future<Output = Result<MemoryId, LaserError>> + Send + 'a>,
558 > {
559 Box::pin(Memory::improve(self, scope, feedback))
560 }
561
562 fn forget<'a>(
563 &'a self,
564 scope: &'a MemoryScope,
565 id: MemoryId,
566 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), LaserError>> + Send + 'a>>
567 {
568 Box::pin(Memory::forget(self, scope, id))
569 }
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize)]
576enum MemoryLogEntry {
577 Item {
579 id: MemoryId,
580 kind: MemoryKind,
581 body: Vec<u8>,
582 #[serde(default, skip_serializing_if = "Option::is_none")]
583 origin: Option<SourceRef>,
584 #[serde(default, skip_serializing_if = "Option::is_none")]
585 producer: Option<laser_wire::graph::ProducerInfo>,
586 },
587 Forget {
590 target: MemoryId,
591 #[serde(default, skip_serializing_if = "Option::is_none")]
592 conversation: Option<ConversationId>,
593 },
594 Feedback {
598 target: MemoryId,
599 weight: f32,
600 #[serde(default, skip_serializing_if = "Option::is_none")]
601 conversation: Option<ConversationId>,
602 },
603}
604
605impl MemoryLogEntry {
606 fn encode(&self) -> Result<Vec<u8>, LaserError> {
607 laser_wire::framing::encode_named(self)
608 .map_err(|error| LaserError::Codec(format!("encode memory entry: {error}")))
609 }
610
611 fn decode(payload: &[u8]) -> Result<Self, LaserError> {
612 laser_wire::framing::decode_named(payload)
613 .map_err(|error| LaserError::Codec(format!("decode memory entry: {error}")))
614 }
615}
616
617pub struct LogMemory {
629 laser: Laser,
630 stream: Option<String>,
632 topic: Identifier,
635 namespace: String,
639 projection: Mutex<Projection>,
640}
641
642#[derive(Default)]
646struct Projection {
647 items: Vec<MemoryItem>,
648 scopes: std::collections::HashMap<MemoryId, StoredMemoryScope>,
649 forgotten: HashSet<(MemoryId, Option<ConversationId>)>,
652 feedback: std::collections::HashMap<(MemoryId, Option<ConversationId>), f32>,
653 named: std::collections::HashMap<String, Vec<u8>>,
657 offsets: Vec<u64>,
658}
659
660#[derive(Default)]
661struct StoredMemoryScope {
662 user: Option<String>,
663 app: Option<String>,
664}
665
666impl Projection {
667 #[cfg(test)]
673 fn absorb(&mut self, payload: &[u8], provenance: Provenance) {
674 self.absorb_scoped(payload, provenance, StoredMemoryScope::default(), None);
675 }
676
677 fn absorb_scoped(
678 &mut self,
679 payload: &[u8],
680 provenance: Provenance,
681 scope: StoredMemoryScope,
682 source: Option<SourceRef>,
683 ) {
684 let Ok(entry) = laser_wire::framing::decode_named::<MemoryLogEntry>(payload) else {
688 self.absorb_named(payload);
689 return;
690 };
691 match entry {
692 MemoryLogEntry::Forget {
693 target,
694 conversation,
695 } => {
696 let before = self.items.len();
700 self.items.retain(|item| {
701 item.id != target
702 || conversation
703 .is_some_and(|scope| scope != item.provenance.conversation_id)
704 });
705 if self.items.len() != before {
706 self.scopes.remove(&target);
707 }
708 self.forgotten.insert((target, conversation));
709 }
710 MemoryLogEntry::Feedback {
711 target,
712 weight,
713 conversation,
714 } => {
715 *self.feedback.entry((target, conversation)).or_insert(0.0) += weight;
716 }
717 MemoryLogEntry::Item {
718 id,
719 kind,
720 body,
721 origin,
722 producer,
723 } => {
724 if self.is_forgotten(id, provenance.conversation_id) {
725 return;
726 }
727 if self.items.iter().any(|item| item.id == id) {
730 return;
731 }
732 self.scopes.insert(id, scope);
733 self.items.push(MemoryItem {
734 id,
735 payload: body,
736 provenance,
737 source,
738 kind,
739 score: None,
740 signals: Vec::new(),
741 origin,
742 producer,
743 });
744 }
745 }
746 }
747
748 fn is_forgotten(&self, id: MemoryId, conversation: ConversationId) -> bool {
749 self.forgotten.contains(&(id, None)) || self.forgotten.contains(&(id, Some(conversation)))
750 }
751
752 fn feedback_for(&self, item: &MemoryItem) -> Option<f32> {
753 let any = self.feedback.get(&(item.id, None));
754 let scoped = self
755 .feedback
756 .get(&(item.id, Some(item.provenance.conversation_id)));
757 match (any, scoped) {
758 (None, None) => None,
759 (any, scoped) => Some(any.copied().unwrap_or(0.0) + scoped.copied().unwrap_or(0.0)),
760 }
761 }
762
763 fn absorb_named(&mut self, payload: &[u8]) {
767 let Ok(record) =
768 laser_wire::framing::decode_named::<laser_wire::memory::MemoryRecord>(payload)
769 else {
770 return;
771 };
772 match record {
773 laser_wire::memory::MemoryRecord::Item { id, body, .. } => {
774 self.named.insert(id, body);
775 }
776 laser_wire::memory::MemoryRecord::Forget { target, .. } => {
777 self.named.remove(&target);
778 }
779 laser_wire::memory::MemoryRecord::Feedback { .. } => {}
780 }
781 }
782}
783
784fn folded_memory_items(
785 projection: &Projection,
786 scope: &MemoryScope,
787 query: &MemoryQuery,
788) -> Vec<MemoryItem> {
789 let agent_filter = query.agent.as_ref().or(scope.agent.as_ref());
790 let mut items: Vec<MemoryItem> = projection
791 .items
792 .iter()
793 .filter(|item| {
794 scope
795 .conversation
796 .is_none_or(|conversation| item.provenance.conversation_id == conversation)
797 })
798 .filter(|item| !projection.is_forgotten(item.id, item.provenance.conversation_id))
799 .filter(|item| {
800 let stored = projection.scopes.get(&item.id);
801 scope.user.as_ref().is_none_or(|expected| {
802 stored.and_then(|value| value.user.as_ref()) == Some(expected)
803 }) && scope.app.as_ref().is_none_or(|expected| {
804 stored.and_then(|value| value.app.as_ref()) == Some(expected)
805 })
806 })
807 .filter(|item| {
808 agent_filter.is_none_or(|agent| item.provenance.agent.as_ref() == Some(agent))
809 })
810 .filter(|item| memory_query_matches(query, &item.payload))
811 .cloned()
812 .collect();
813 items.reverse();
814 if query.strategy != RecallStrategy::Recent && !projection.feedback.is_empty() {
816 for item in &mut items {
817 if let Some(weight) = projection.feedback_for(item) {
818 item.score = Some(weight);
819 }
820 }
821 items.sort_by(|left, right| {
822 right
823 .score
824 .unwrap_or(0.0)
825 .total_cmp(&left.score.unwrap_or(0.0))
826 });
827 for (rank, item) in items.iter_mut().enumerate() {
828 if let Some(score) = item.score {
829 item.signals.push(RecallSignal {
832 strategy: RecallStrategy::Auto,
833 rank,
834 score: Some(score),
835 });
836 }
837 }
838 }
839 items.truncate(query.limit);
840 items
841}
842
843impl LogMemory {
844 fn stream_name(&self) -> Result<&str, LaserError> {
845 match self.stream.as_deref() {
846 Some(stream) => Ok(stream),
847 None => self.laser.stream_required(),
848 }
849 }
850
851 fn resource_namespace(&self) -> String {
855 self.laser
856 .resource_name_in(self.resource_stream(), &self.namespace)
857 }
858
859 fn resource_stream(&self) -> Option<&str> {
860 self.laser
861 .resource_scope(self.stream.as_deref().or(self.laser.default_stream()))
862 }
863
864 pub fn new(laser: Laser) -> Self {
870 Self {
871 laser,
872 stream: None,
873 topic: AgentTopic::Memory.as_identifier(),
874 namespace: AgentTopic::Memory.name().unwrap_or("memory").to_owned(),
875 projection: Mutex::new(Projection::default()),
876 }
877 }
878
879 pub fn in_namespace(laser: Laser, namespace: impl Into<String>) -> Self {
883 Self {
884 laser,
885 stream: None,
886 topic: AgentTopic::Memory.as_identifier(),
887 namespace: namespace.into(),
888 projection: Mutex::new(Projection::default()),
889 }
890 }
891
892 pub fn on_topic(laser: Laser, topic: Identifier) -> Self {
897 Self::on_stream_topic(laser, None, topic)
898 }
899
900 pub fn on_stream_topic(laser: Laser, stream: Option<String>, topic: Identifier) -> Self {
903 let namespace = topic.get_string_value().unwrap_or_default();
904 Self {
905 laser,
906 stream,
907 topic,
908 namespace,
909 projection: Mutex::new(Projection::default()),
910 }
911 }
912
913 pub fn on_topic_named(laser: Laser, topic: &str) -> Result<Self, LaserError> {
915 Ok(Self::on_topic(laser, Identifier::named(topic)?))
916 }
917
918 pub fn on_stream_topic_named(
921 laser: Laser,
922 stream: Option<String>,
923 topic: &str,
924 ) -> Result<Self, LaserError> {
925 Ok(Self::on_stream_topic(
926 laser,
927 stream,
928 Identifier::named(topic)?,
929 ))
930 }
931
932 async fn govern_payload(
933 &self,
934 provenance: &Provenance,
935 payload: Vec<u8>,
936 ) -> Result<Vec<u8>, LaserError> {
937 let topic = self.topic.to_string();
938 let action = crate::govern::GovernedAction {
939 kind: crate::govern::ActionKind::MemoryWrite,
940 stream: self.stream_name()?,
941 topic: &topic,
942 source: provenance.agent.as_ref().map(|agent| agent.as_str()),
943 target: None,
944 conversation: Some(provenance.conversation_id),
945 correlation: None,
946 operation: None,
947 tool: None,
948 on_behalf_of: None,
949 purpose: None,
950 data_classification: None,
951 payload: &payload,
952 signed: false,
953 counters: crate::govern::ActionCounters::default(),
954 };
955 Ok(self.laser.govern(action).await?.unwrap_or(payload))
956 }
957
958 async fn publish_unchecked(
961 &self,
962 provenance: &Provenance,
963 scope: &MemoryScope,
964 payload: Vec<u8>,
965 ) -> Result<(), LaserError> {
966 let mut headers: std::collections::BTreeMap<HeaderKey, HeaderValue> =
967 provenance.try_into()?;
968 put_header(
972 &mut headers,
973 laser_wire::headers::MEMORY_NAMESPACE,
974 &self.resource_namespace(),
975 )?;
976 if let Some(user) = &scope.user {
977 put_header(&mut headers, laser_wire::headers::MEMORY_USER, user)?;
978 }
979 if let Some(app) = &scope.app {
980 put_header(&mut headers, laser_wire::headers::MEMORY_APP, app)?;
981 }
982 let key = provenance.partition_key();
983 let topic = self.topic.to_string();
984 match &self.stream {
985 Some(stream) => self
986 .laser
987 .send_with_headers_on(stream, &topic, payload, headers, Some(&key))
988 .await
989 .map(|_| ()),
990 None => self
991 .laser
992 .send_with_headers(&topic, payload, headers, Some(&key))
993 .await
994 .map(|_| ()),
995 }
996 }
997
998 async fn publish(
999 &self,
1000 provenance: &Provenance,
1001 scope: &MemoryScope,
1002 payload: Vec<u8>,
1003 ) -> Result<(), LaserError> {
1004 let payload = self.govern_payload(provenance, payload).await?;
1005 self.publish_unchecked(provenance, scope, payload).await
1006 }
1007
1008 fn provenance(scope: &MemoryScope, idempotency_key: String) -> Provenance {
1009 let mut provenance = Provenance::builder()
1010 .conversation_id(scope.conversation.unwrap_or_default())
1011 .build();
1012 provenance.agent = scope.agent.clone();
1013 provenance.idempotency_key = Some(idempotency_key);
1014 provenance
1015 }
1016
1017 fn named_provenance(id: String) -> Provenance {
1023 let mut provenance = Provenance::builder()
1024 .conversation_id(ConversationId::derive(&id))
1025 .build();
1026 provenance.idempotency_key = Some(id);
1027 provenance
1028 }
1029
1030 pub(crate) async fn append(
1034 &self,
1035 scope: &MemoryScope,
1036 id: MemoryId,
1037 kind: MemoryKind,
1038 payload: Vec<u8>,
1039 ) -> Result<MemoryId, LaserError> {
1040 let provenance = Self::provenance(scope, id.to_string());
1041 let body = self.govern_payload(&provenance, payload).await?;
1042 let entry = MemoryLogEntry::Item {
1043 id,
1044 kind,
1045 body,
1046 origin: scope.origin.clone(),
1047 producer: scope.producer.clone(),
1048 };
1049 self.publish_unchecked(&provenance, scope, entry.encode()?)
1050 .await?;
1051 Ok(id)
1052 }
1053
1054 fn named_key(&self, key: &str) -> String {
1057 format!("{}/{}", self.namespace, key)
1058 }
1059
1060 pub async fn set_named(&self, key: &str, body: Vec<u8>) -> Result<(), LaserError> {
1063 let id = self.named_key(key);
1064 let record = laser_wire::memory::MemoryRecord::Item {
1065 origin: None,
1066 producer: None,
1067 id: id.clone(),
1068 kind: "fact".to_owned(),
1070 body: body.clone(),
1071 };
1072 let payload = laser_wire::framing::encode_named(&record)
1073 .map_err(|error| LaserError::Codec(format!("encode named memory item: {error}")))?;
1074 let scope = MemoryScope::default();
1075 let provenance = Self::named_provenance(id.clone());
1076 self.publish(&provenance, &scope, payload).await?;
1077 self.projection.lock().await.named.insert(id, body);
1078 Ok(())
1079 }
1080
1081 pub async fn update_named(&self, key: &str, patch: Vec<u8>) -> Result<(), LaserError> {
1084 let current = self.fetch_named_folded(key).await?;
1087 let merged = merge_named(current.as_deref(), &patch)?;
1088 self.set_named(key, merged).await
1089 }
1090
1091 pub async fn forget_named(&self, key: &str) -> Result<(), LaserError> {
1094 let id = self.named_key(key);
1095 let record = laser_wire::memory::MemoryRecord::Forget {
1096 target: id.clone(),
1097 conversation: None,
1098 };
1099 let payload = laser_wire::framing::encode_named(&record)
1100 .map_err(|error| LaserError::Codec(format!("encode named memory forget: {error}")))?;
1101 let scope = MemoryScope::default();
1102 let provenance = Self::named_provenance(id.clone());
1103 self.publish(&provenance, &scope, payload).await?;
1104 self.projection.lock().await.named.remove(&id);
1105 Ok(())
1106 }
1107
1108 pub async fn fetch_named(&self, key: &str) -> Result<Option<Vec<u8>>, LaserError> {
1111 #[cfg(feature = "kv")]
1114 {
1115 self.laser
1116 .kv(self.resource_namespace())
1117 .get(self.named_key(key))
1118 .await
1119 }
1120 #[cfg(not(feature = "kv"))]
1121 {
1122 let _ = key;
1123 Err(LaserError::unsupported(
1124 "memory fetch",
1125 "named point-state read reads the managed key-value view. Build with the `kv` \
1126 feature, or call `fetch_named_folded` to fold the topic in process",
1127 ))
1128 }
1129 }
1130
1131 pub async fn fetch_named_folded(&self, key: &str) -> Result<Option<Vec<u8>>, LaserError> {
1135 self.catch_up().await?;
1136 let id = self.named_key(key);
1137 Ok(self.projection.lock().await.named.get(&id).cloned())
1138 }
1139
1140 async fn catch_up(&self) -> Result<(), LaserError> {
1150 let stream = Identifier::named(self.stream_name()?)?;
1151 let topic = self.topic.clone();
1152 let Some(details) = self.laser.client().get_topic(&stream, &topic).await? else {
1153 return Ok(());
1154 };
1155 let stream_id = self
1156 .laser
1157 .client()
1158 .get_stream(&stream)
1159 .await?
1160 .ok_or_else(|| LaserError::Invalid("memory stream is missing".to_owned()))?
1161 .id;
1162 let partitions = crate::poll::bounded_partitions(details.partitions_count);
1163 let consumer = Consumer::new(Identifier::named("laser-log-memory")?);
1164 let namespace = self.resource_namespace();
1165
1166 let from = {
1167 let mut projection = self.projection.lock().await;
1168 if projection.offsets.len() < partitions as usize {
1169 projection.offsets.resize(partitions as usize, 0);
1170 }
1171 projection.offsets.clone()
1172 };
1173
1174 let mut drained: Vec<(u32, u64, Vec<IggyMessage>)> = Vec::new();
1175 for partition in 0..partitions {
1176 let batch = crate::poll::drain_partition(
1177 &self.laser.client(),
1178 &stream,
1179 &topic,
1180 &consumer,
1181 crate::poll::DrainRange::open(partition, from[partition as usize]),
1182 READ_BATCH,
1183 )
1184 .await?;
1185 if !batch.messages.is_empty() {
1186 drained.push((partition, batch.next_offset, batch.messages));
1187 }
1188 }
1189 if drained.is_empty() {
1190 return Ok(());
1191 }
1192
1193 let mut projection = self.projection.lock().await;
1194 if projection.offsets.len() < partitions as usize {
1195 projection.offsets.resize(partitions as usize, 0);
1196 }
1197 for (partition, next_offset, messages) in drained {
1198 if projection.offsets[partition as usize] != from[partition as usize] {
1199 continue; }
1201 for message in messages {
1202 let Ok(Some(headers)) = message.user_headers_map() else {
1205 continue;
1206 };
1207 let Ok(provenance) = crate::provenance::provenance_from_headers(&headers) else {
1208 continue;
1209 };
1210 let text = |name: &str| -> Option<String> {
1211 let key = HeaderKey::from_str(name).ok()?;
1212 headers.get(&key)?.as_str().ok().map(str::to_owned)
1213 };
1214 if text(laser_wire::headers::MEMORY_NAMESPACE).as_deref()
1215 != Some(namespace.as_str())
1216 {
1217 continue;
1218 }
1219 let source = SourceRef::Message {
1220 generation: Some(details.created_at.as_micros()),
1221 stream: stream_id,
1222 topic: details.id,
1223 partition,
1224 offset: message.header.offset,
1225 conversation: Some(provenance.conversation_id.to_string()),
1226 };
1227 projection.absorb_scoped(
1228 &message.payload,
1229 provenance,
1230 StoredMemoryScope {
1231 user: text(laser_wire::headers::MEMORY_USER),
1232 app: text(laser_wire::headers::MEMORY_APP),
1233 },
1234 Some(source),
1235 );
1236 }
1237 projection.offsets[partition as usize] = next_offset;
1238 }
1239 Ok(())
1240 }
1241
1242 pub async fn recall_folded(
1248 &self,
1249 scope: &MemoryScope,
1250 query: &MemoryQuery,
1251 ) -> Result<Vec<MemoryItem>, LaserError> {
1252 if scope
1253 .stream
1254 .as_deref()
1255 .is_some_and(|stream| self.stream_name().ok() != Some(stream))
1256 {
1257 return Ok(Vec::new());
1258 }
1259 self.catch_up().await?;
1260 let projection = self.projection.lock().await;
1261 Ok(folded_memory_items(&projection, scope, query))
1272 }
1273
1274 #[cfg(feature = "kv")]
1279 async fn recall_kv(
1280 &self,
1281 scope: &MemoryScope,
1282 query: &MemoryQuery,
1283 ) -> Result<Vec<MemoryItem>, LaserError> {
1284 if scope
1285 .stream
1286 .as_deref()
1287 .is_some_and(|stream| self.stream_name().ok() != Some(stream))
1288 {
1289 return Ok(Vec::new());
1290 }
1291 let kv = self.laser.kv(self.resource_namespace());
1292 let lens_stream = self.resource_stream().map(str::to_owned);
1293 recall_kv_pages(scope, query, |cursor| {
1294 let mut scan = kv.scan();
1295 if let Some(conversation) = scope.conversation {
1296 scan = scan
1297 .conversation(conversation)
1298 .lens_stream(lens_stream.clone());
1299 }
1300 if let Some(cursor) = cursor {
1301 scan = scan.cursor(cursor);
1302 }
1303 async move { scan.fetch().await }
1304 })
1305 .await
1306 }
1307}
1308
1309#[cfg(feature = "kv")]
1310async fn recall_kv_pages<F, Fut>(
1311 scope: &MemoryScope,
1312 query: &MemoryQuery,
1313 mut fetch: F,
1314) -> Result<Vec<MemoryItem>, LaserError>
1315where
1316 F: FnMut(Option<Vec<u8>>) -> Fut,
1317 Fut: std::future::Future<Output = Result<laser_wire::kv::KvPage, LaserError>>,
1318{
1319 if query.limit == 0 {
1320 return Ok(Vec::new());
1321 }
1322 let mut selected = std::collections::BTreeMap::new();
1323 let mut cursor = None;
1324 loop {
1325 let page = fetch(cursor.clone()).await?;
1326 for entry in page.entries {
1327 let arrival = arrival_key(&entry);
1328 if let Some(item) = memory_item_from_entry(scope, query.agent.as_ref(), entry)
1329 .filter(|item| memory_query_matches(query, &item.payload))
1330 {
1331 selected.insert((arrival, item.id), item);
1332 if selected.len() > query.limit {
1333 selected.pop_first();
1334 }
1335 }
1336 }
1337 let Some(next) = page.cursor else { break };
1338 if cursor.as_ref() == Some(&next) {
1339 return Err(LaserError::Protocol(
1340 "memory scan cursor did not advance".to_owned(),
1341 ));
1342 }
1343 cursor = Some(next);
1344 }
1345 Ok(selected.into_values().rev().collect())
1346}
1347
1348#[cfg(feature = "kv")]
1352fn arrival_key(entry: &laser_wire::kv::KvEntry) -> (u64, u32, u32, u64) {
1353 let scope = entry.scope.as_deref();
1354 let timestamp = scope.and_then(|scope| scope.timestamp_micros).unwrap_or(0);
1355 match scope.and_then(|scope| scope.source.as_ref()) {
1356 Some(SourceRef::Message {
1357 topic,
1358 partition,
1359 offset,
1360 ..
1361 }) => (timestamp, *topic, *partition, *offset),
1362 _ => (timestamp, 0, 0, 0),
1363 }
1364}
1365
1366#[cfg(feature = "kv")]
1367fn memory_item_from_entry(
1368 scope: &MemoryScope,
1369 agent: Option<&AgentId>,
1370 entry: laser_wire::kv::KvEntry,
1371) -> Option<MemoryItem> {
1372 let id: MemoryId = entry.key_str()?.parse().ok()?;
1373 let stored = *entry.scope?;
1374 let conversation = match stored.conversation.as_deref() {
1375 Some(value) => value.parse().ok()?,
1376 None => scope.conversation?,
1377 };
1378 let agent = agent.or(scope.agent.as_ref());
1379 if scope
1380 .conversation
1381 .is_some_and(|expected| expected != conversation)
1382 || scope
1383 .user
1384 .as_ref()
1385 .is_some_and(|expected| stored.user.as_ref() != Some(expected))
1386 || scope
1387 .app
1388 .as_ref()
1389 .is_some_and(|expected| stored.app.as_ref() != Some(expected))
1390 || agent.is_some_and(|expected| stored.agent.as_deref() != Some(expected.as_str()))
1391 {
1392 return None;
1393 }
1394 let kind = stored
1395 .kind
1396 .as_deref()
1397 .map(MemoryKind::from_word)
1398 .unwrap_or_default();
1399 let mut provenance = Provenance::builder().conversation_id(conversation).build();
1400 provenance.agent = stored.agent.as_deref().and_then(|value| value.parse().ok());
1401 Some(MemoryItem {
1402 id,
1403 payload: entry.value,
1404 provenance,
1405 kind,
1406 source: stored.source,
1407 score: None,
1408 signals: Vec::new(),
1409 origin: None,
1410 producer: None,
1411 })
1412}
1413
1414impl Memory for LogMemory {
1415 async fn remember(
1416 &self,
1417 scope: &MemoryScope,
1418 payload: Vec<u8>,
1419 ) -> Result<MemoryId, LaserError> {
1420 self.append(scope, MemoryId::new(), MemoryKind::Fact, payload)
1421 .await
1422 }
1423
1424 async fn append(
1425 &self,
1426 scope: &MemoryScope,
1427 id: MemoryId,
1428 kind: MemoryKind,
1429 payload: Vec<u8>,
1430 ) -> Result<MemoryId, LaserError> {
1431 LogMemory::append(self, scope, id, kind, payload).await
1432 }
1433
1434 async fn recall(
1435 &self,
1436 scope: &MemoryScope,
1437 query: &MemoryQuery,
1438 ) -> Result<Vec<MemoryItem>, LaserError> {
1439 #[cfg(feature = "kv")]
1444 {
1445 self.recall_kv(scope, query).await
1446 }
1447 #[cfg(not(feature = "kv"))]
1448 {
1449 let _ = (scope, query);
1450 Err(LaserError::unsupported(
1451 "memory recall",
1452 "durable recall reads the managed key-value view. Build with the `kv` feature, \
1453 or call `recall_folded` to fold the topic in process",
1454 ))
1455 }
1456 }
1457
1458 async fn improve(
1459 &self,
1460 scope: &MemoryScope,
1461 feedback: Feedback,
1462 ) -> Result<MemoryId, LaserError> {
1463 let id = MemoryId::new();
1467 let entry = MemoryLogEntry::Feedback {
1468 target: feedback.target,
1469 weight: feedback.weight,
1470 conversation: scope.conversation,
1471 };
1472 let provenance = Self::provenance(scope, id.to_string());
1473 self.publish(&provenance, scope, entry.encode()?).await?;
1474 Ok(id)
1475 }
1476
1477 async fn forget(&self, scope: &MemoryScope, id: MemoryId) -> Result<(), LaserError> {
1478 let entry = MemoryLogEntry::Forget {
1479 target: id,
1480 conversation: scope.conversation,
1481 };
1482 let provenance = Self::provenance(scope, MemoryId::new().to_string());
1483 self.publish(&provenance, scope, entry.encode()?).await?;
1484 Ok(())
1485 }
1486}
1487
1488#[trait_variant::make(Embedder: Send)]
1492pub trait LocalEmbedder {
1493 async fn embed(&self, text: &str) -> Result<Vec<f32>, LaserError>;
1494}
1495
1496#[trait_variant::make(Reranker: Send)]
1503pub trait LocalReranker {
1504 async fn rerank(
1505 &self,
1506 query: &str,
1507 items: Vec<MemoryItem>,
1508 ) -> Result<Vec<MemoryItem>, LaserError>;
1509}
1510
1511#[trait_variant::make(Summarizer: Send)]
1516pub trait LocalSummarizer {
1517 async fn summarize(&self, bodies: Vec<Vec<u8>>) -> Result<Vec<u8>, LaserError>;
1518}
1519
1520#[derive(Debug, Clone, Default, PartialEq, Eq)]
1522pub struct ConsolidationReport {
1523 pub summarized: usize,
1525 pub reweighted: usize,
1527 pub pruned: usize,
1529 pub derived: usize,
1531}
1532
1533#[trait_variant::make(Consolidator: Send)]
1541pub trait LocalConsolidator {
1542 async fn consolidate(&self, scope: &MemoryScope) -> Result<ConsolidationReport, LaserError>;
1544}
1545
1546pub struct DefaultConsolidator<M, S = NoSummarizer> {
1554 memory: M,
1555 max_items: usize,
1556 summarizer: Option<S>,
1557 prune_summarized: bool,
1558}
1559
1560pub struct NoSummarizer;
1563
1564impl Summarizer for NoSummarizer {
1565 async fn summarize(&self, _bodies: Vec<Vec<u8>>) -> Result<Vec<u8>, LaserError> {
1566 Err(LaserError::Config(
1567 "no summarizer was registered on this consolidator",
1568 ))
1569 }
1570}
1571
1572impl<M> DefaultConsolidator<M> {
1573 pub fn new(memory: M, max_items: usize) -> Self {
1575 Self {
1576 memory,
1577 max_items,
1578 summarizer: None,
1579 prune_summarized: false,
1580 }
1581 }
1582}
1583
1584impl<M, S> DefaultConsolidator<M, S> {
1585 pub fn with_summarizer<S2>(self, summarizer: S2) -> DefaultConsolidator<M, S2> {
1590 DefaultConsolidator {
1591 memory: self.memory,
1592 max_items: self.max_items,
1593 summarizer: Some(summarizer),
1594 prune_summarized: self.prune_summarized,
1595 }
1596 }
1597
1598 #[must_use]
1601 pub fn prune_summarized(mut self) -> Self {
1602 self.prune_summarized = true;
1603 self
1604 }
1605}
1606
1607impl<M: Memory + Sync, S: Summarizer + Sync> Consolidator for DefaultConsolidator<M, S> {
1608 async fn consolidate(&self, scope: &MemoryScope) -> Result<ConsolidationReport, LaserError> {
1609 let query = MemoryQuery::builder()
1610 .limit(CONSOLIDATION_WINDOW)
1611 .strategy(RecallStrategy::Recent)
1612 .build();
1613 let mut items = self.memory.recall(scope, &query).await?;
1614 let mut report = ConsolidationReport::default();
1615
1616 if let Some(summarizer) = &self.summarizer {
1620 let sessions: Vec<&MemoryItem> = items
1621 .iter()
1622 .filter(|item| item.kind == MemoryKind::Message)
1623 .collect();
1624 if !sessions.is_empty() {
1625 for (conversation, turns) in by_conversation(&sessions) {
1626 let bodies = turns.iter().map(|item| item.payload.clone()).collect();
1627 let summary = summarizer.summarize(bodies).await?;
1628 let mut summary_scope = scope.clone();
1629 summary_scope.conversation = Some(conversation);
1630 summary_scope.lifetime = Lifetime::Durable;
1631 self.memory
1632 .append(
1633 &summary_scope,
1634 MemoryId::new(),
1635 MemoryKind::Summary,
1636 summary,
1637 )
1638 .await?;
1639 }
1640 report.summarized = sessions.len();
1641 if self.prune_summarized {
1642 let ids: Vec<MemoryId> = sessions.iter().map(|item| item.id).collect();
1643 for id in ids {
1644 if self.memory.forget(scope, id).await.is_ok() {
1645 report.pruned += 1;
1646 }
1647 }
1648 items.retain(|item| item.kind != MemoryKind::Message);
1649 }
1650 }
1651 }
1652
1653 if items.len() <= self.max_items {
1654 return Ok(report);
1655 }
1656 let prune_count = items.len() - self.max_items;
1658 for item in items.into_iter().rev().take(prune_count) {
1659 if self.memory.forget(scope, item.id).await.is_ok() {
1660 report.pruned += 1;
1661 }
1662 }
1663 Ok(report)
1664 }
1665}
1666
1667fn by_conversation<'a>(items: &[&'a MemoryItem]) -> Vec<(ConversationId, Vec<&'a MemoryItem>)> {
1670 let mut groups: Vec<(ConversationId, Vec<&'a MemoryItem>)> = Vec::new();
1671 for &item in items {
1672 let conversation = item.provenance.conversation_id;
1673 match groups.iter_mut().find(|(known, _)| *known == conversation) {
1674 Some((_, turns)) => turns.push(item),
1675 None => groups.push((conversation, vec![item])),
1676 }
1677 }
1678 groups
1679}
1680
1681pub struct RerankedMemory<M, R> {
1687 inner: M,
1688 reranker: R,
1689}
1690
1691impl<M, R> RerankedMemory<M, R> {
1692 pub fn new(inner: M, reranker: R) -> Self {
1694 Self { inner, reranker }
1695 }
1696}
1697
1698impl<M: Memory + Sync, R: Reranker + Sync> Memory for RerankedMemory<M, R> {
1699 async fn remember(
1700 &self,
1701 scope: &MemoryScope,
1702 payload: Vec<u8>,
1703 ) -> Result<MemoryId, LaserError> {
1704 self.inner.remember(scope, payload).await
1705 }
1706
1707 async fn append(
1708 &self,
1709 scope: &MemoryScope,
1710 id: MemoryId,
1711 kind: MemoryKind,
1712 payload: Vec<u8>,
1713 ) -> Result<MemoryId, LaserError> {
1714 self.inner.append(scope, id, kind, payload).await
1715 }
1716
1717 async fn recall(
1718 &self,
1719 scope: &MemoryScope,
1720 query: &MemoryQuery,
1721 ) -> Result<Vec<MemoryItem>, LaserError> {
1722 let candidates = self.inner.recall(scope, query).await?;
1723 match &query.semantic {
1724 Some(text) => self.reranker.rerank(text, candidates).await,
1725 None => Ok(candidates),
1726 }
1727 }
1728
1729 async fn improve(
1730 &self,
1731 scope: &MemoryScope,
1732 feedback: Feedback,
1733 ) -> Result<MemoryId, LaserError> {
1734 self.inner.improve(scope, feedback).await
1735 }
1736
1737 async fn forget(&self, scope: &MemoryScope, id: MemoryId) -> Result<(), LaserError> {
1738 self.inner.forget(scope, id).await
1739 }
1740}
1741
1742pub struct VectorMemory<E> {
1746 embedder: E,
1747 items: Mutex<Vec<VectorEntry>>,
1748 governance: Option<VectorGovernance>,
1749}
1750
1751#[derive(Clone)]
1752struct VectorGovernance {
1753 laser: Laser,
1754}
1755
1756struct VectorEntry {
1757 id: MemoryId,
1758 scope: MemoryScope,
1759 embedding: Vec<f32>,
1760 item: MemoryItem,
1761 feedback: f32,
1765}
1766
1767impl<E> VectorMemory<E> {
1768 pub fn new(embedder: E) -> Self {
1770 Self::with_governance(embedder, None)
1771 }
1772
1773 pub fn governed(laser: Laser, embedder: E) -> Self {
1778 Self::with_governance(embedder, Some(VectorGovernance { laser }))
1779 }
1780
1781 fn with_governance(embedder: E, governance: Option<VectorGovernance>) -> Self {
1782 Self {
1783 embedder,
1784 items: Mutex::new(Vec::new()),
1785 governance,
1786 }
1787 }
1788
1789 async fn govern_entry(
1790 &self,
1791 scope: &MemoryScope,
1792 entry: MemoryLogEntry,
1793 ) -> Result<MemoryLogEntry, LaserError> {
1794 let Some(governance) = &self.governance else {
1795 return Ok(entry);
1796 };
1797 let topic = AgentTopic::Memory.topic_string();
1798 let payload = match &entry {
1799 MemoryLogEntry::Item { body, .. } => body.clone(),
1800 MemoryLogEntry::Forget { .. } | MemoryLogEntry::Feedback { .. } => entry.encode()?,
1801 };
1802 let conversation = scope.conversation.unwrap_or_default();
1803 let action = crate::govern::GovernedAction {
1804 kind: crate::govern::ActionKind::MemoryWrite,
1805 stream: governance.laser.stream_required()?,
1806 topic: &topic,
1807 source: scope.agent.as_ref().map(|agent| agent.as_str()),
1808 target: None,
1809 conversation: Some(conversation),
1810 correlation: None,
1811 operation: None,
1812 tool: None,
1813 on_behalf_of: None,
1814 purpose: None,
1815 data_classification: None,
1816 payload: &payload,
1817 signed: false,
1818 counters: crate::govern::ActionCounters::default(),
1819 };
1820 match governance.laser.govern(action).await? {
1821 Some(modified) => match entry {
1822 MemoryLogEntry::Item {
1823 id,
1824 kind,
1825 origin,
1826 producer,
1827 ..
1828 } => Ok(MemoryLogEntry::Item {
1829 id,
1830 kind,
1831 body: modified,
1832 origin,
1833 producer,
1834 }),
1835 MemoryLogEntry::Forget { .. } | MemoryLogEntry::Feedback { .. } => {
1836 MemoryLogEntry::decode(&modified)
1837 }
1838 },
1839 None => Ok(entry),
1840 }
1841 }
1842
1843 async fn append(
1844 &self,
1845 scope: &MemoryScope,
1846 id: MemoryId,
1847 kind: MemoryKind,
1848 payload: Vec<u8>,
1849 ) -> Result<MemoryId, LaserError>
1850 where
1851 E: Embedder + Sync,
1852 {
1853 let entry = self
1854 .govern_entry(
1855 scope,
1856 MemoryLogEntry::Item {
1857 id,
1858 kind,
1859 body: payload,
1860 origin: scope.origin.clone(),
1861 producer: scope.producer.clone(),
1862 },
1863 )
1864 .await?;
1865 let MemoryLogEntry::Item {
1866 id,
1867 kind,
1868 body,
1869 origin,
1870 producer,
1871 } = entry
1872 else {
1873 return Err(LaserError::Invalid(
1874 "a memory-item governor modification must remain an item".to_owned(),
1875 ));
1876 };
1877 let embedding = self.embedder.embed(&String::from_utf8_lossy(&body)).await?;
1878 let mut provenance = Provenance::builder()
1879 .conversation_id(scope.conversation.unwrap_or_default())
1880 .build();
1881 provenance.agent = scope.agent.clone();
1882 provenance.idempotency_key = Some(id.to_string());
1883 let item = MemoryItem {
1884 id,
1885 payload: body,
1886 provenance,
1887 kind,
1888 score: None,
1889 signals: Vec::new(),
1890 source: None,
1891 origin,
1892 producer,
1893 };
1894 let mut items = self.items.lock().await;
1895 if items.iter().any(|entry| entry.id == id) {
1896 return Ok(id);
1897 }
1898 items.push(VectorEntry {
1899 id,
1900 scope: scope.clone(),
1901 embedding,
1902 item,
1903 feedback: 0.0,
1904 });
1905 Ok(id)
1906 }
1907}
1908
1909impl<E: Embedder + Sync> Memory for VectorMemory<E> {
1910 async fn remember(
1911 &self,
1912 scope: &MemoryScope,
1913 payload: Vec<u8>,
1914 ) -> Result<MemoryId, LaserError> {
1915 self.append(scope, MemoryId::new(), MemoryKind::Fact, payload)
1916 .await
1917 }
1918
1919 async fn append(
1920 &self,
1921 scope: &MemoryScope,
1922 id: MemoryId,
1923 kind: MemoryKind,
1924 payload: Vec<u8>,
1925 ) -> Result<MemoryId, LaserError> {
1926 VectorMemory::append(self, scope, id, kind, payload).await
1927 }
1928
1929 async fn recall(
1930 &self,
1931 scope: &MemoryScope,
1932 query: &MemoryQuery,
1933 ) -> Result<Vec<MemoryItem>, LaserError> {
1934 let wants_semantic = !matches!(
1938 query.strategy,
1939 RecallStrategy::Keyword | RecallStrategy::Recent
1940 );
1941 let wants_keyword = matches!(
1942 query.strategy,
1943 RecallStrategy::Keyword | RecallStrategy::Hybrid
1944 );
1945 let query_embedding = match (&query.semantic, wants_semantic) {
1947 (Some(text), true) => Some(self.embedder.embed(text).await?),
1948 _ => None,
1949 };
1950 let query_tokens = match (&query.semantic, wants_keyword) {
1951 (Some(text), true) => Some(tokenize(text)),
1952 _ => None,
1953 };
1954 let agent_filter = query.agent.as_ref().or(scope.agent.as_ref());
1955 let items = self.items.lock().await;
1956 let matched: Vec<&VectorEntry> = items
1957 .iter()
1958 .filter(|entry| {
1959 scope
1960 .conversation
1961 .is_none_or(|c| entry.scope.conversation == Some(c))
1962 && agent_filter.is_none_or(|a| entry.scope.agent.as_ref() == Some(a))
1963 && scope
1964 .user
1965 .as_ref()
1966 .is_none_or(|u| entry.scope.user.as_ref() == Some(u))
1967 && scope
1968 .app
1969 .as_ref()
1970 .is_none_or(|a| entry.scope.app.as_ref() == Some(a))
1971 && scope
1972 .stream
1973 .as_ref()
1974 .is_none_or(|stream| entry.scope.stream.as_ref() == Some(stream))
1975 })
1976 .collect();
1977
1978 let feedback_ranks = query.strategy != RecallStrategy::Recent;
1980 let any_feedback = feedback_ranks && matched.iter().any(|entry| entry.feedback != 0.0);
1981
1982 let mut scored: Vec<(f32, &VectorEntry, Vec<RecallSignal>)> = matched
1983 .iter()
1984 .map(|&entry| {
1985 let mut signals = Vec::new();
1986 if let Some(embedding) = &query_embedding {
1987 signals.push(RecallSignal {
1988 strategy: RecallStrategy::Semantic,
1989 rank: 0,
1990 score: Some(cosine(embedding, &entry.embedding)),
1991 });
1992 }
1993 if let Some(tokens) = &query_tokens {
1994 signals.push(RecallSignal {
1995 strategy: RecallStrategy::Keyword,
1996 rank: 0,
1997 score: Some(keyword_score(tokens, &entry.item.payload)),
1998 });
1999 }
2000 if feedback_ranks && entry.feedback != 0.0 {
2001 signals.push(RecallSignal {
2002 strategy: RecallStrategy::Auto,
2003 rank: 0,
2004 score: Some(entry.feedback),
2005 });
2006 }
2007 let score = signals
2008 .iter()
2009 .map(|signal| signal.score.unwrap_or(0.0))
2010 .sum();
2011 (score, entry, signals)
2012 })
2013 .collect();
2014 let any_query_signal = query_embedding.is_some() || query_tokens.is_some();
2015 if any_query_signal || any_feedback {
2016 for strategy in [
2017 RecallStrategy::Semantic,
2018 RecallStrategy::Keyword,
2019 RecallStrategy::Auto,
2020 ] {
2021 let mut ranks: Vec<(usize, f32)> = scored
2022 .iter()
2023 .enumerate()
2024 .filter_map(|(index, (_, _, signals))| {
2025 signals
2026 .iter()
2027 .find(|signal| signal.strategy == strategy)
2028 .map(|signal| (index, signal.score.unwrap_or(0.0)))
2029 })
2030 .collect();
2031 ranks.sort_by(|left, right| right.1.total_cmp(&left.1));
2032 for (rank, (index, _)) in ranks.into_iter().enumerate() {
2033 if let Some(signal) = scored[index]
2034 .2
2035 .iter_mut()
2036 .find(|signal| signal.strategy == strategy)
2037 {
2038 signal.rank = rank;
2039 }
2040 }
2041 }
2042 scored.sort_by(|left, right| right.0.total_cmp(&left.0));
2043 Ok(scored
2044 .into_iter()
2045 .take(query.limit)
2046 .map(|(score, entry, signals)| {
2047 let mut item = entry.item.clone();
2048 item.score = Some(score);
2049 item.signals = signals;
2050 item
2051 })
2052 .collect())
2053 } else {
2054 Ok(matched
2055 .into_iter()
2056 .rev()
2057 .take(query.limit)
2058 .map(|entry| entry.item.clone())
2059 .collect())
2060 }
2061 }
2062
2063 async fn improve(
2064 &self,
2065 scope: &MemoryScope,
2066 feedback: Feedback,
2067 ) -> Result<MemoryId, LaserError> {
2068 let entry = self
2069 .govern_entry(
2070 scope,
2071 MemoryLogEntry::Feedback {
2072 target: feedback.target,
2073 weight: feedback.weight,
2074 conversation: scope.conversation,
2075 },
2076 )
2077 .await?;
2078 let MemoryLogEntry::Feedback {
2079 target,
2080 weight,
2081 conversation,
2082 } = entry
2083 else {
2084 return Err(LaserError::Invalid(
2085 "a memory-feedback governor modification must remain feedback".to_owned(),
2086 ));
2087 };
2088 let mut items = self.items.lock().await;
2091 if let Some(entry) = items
2092 .iter_mut()
2093 .find(|entry| entry.id == target && in_conversation(entry, conversation))
2094 {
2095 entry.feedback += weight;
2096 }
2097 Ok(MemoryId::new())
2098 }
2099
2100 async fn forget(&self, scope: &MemoryScope, id: MemoryId) -> Result<(), LaserError> {
2101 let entry = self
2102 .govern_entry(
2103 scope,
2104 MemoryLogEntry::Forget {
2105 target: id,
2106 conversation: scope.conversation,
2107 },
2108 )
2109 .await?;
2110 let MemoryLogEntry::Forget {
2111 target,
2112 conversation,
2113 } = entry
2114 else {
2115 return Err(LaserError::Invalid(
2116 "a memory-forget governor modification must remain a tombstone".to_owned(),
2117 ));
2118 };
2119 self.items
2120 .lock()
2121 .await
2122 .retain(|entry| entry.id != target || !in_conversation(entry, conversation));
2123 Ok(())
2124 }
2125}
2126
2127fn in_conversation(entry: &VectorEntry, conversation: Option<ConversationId>) -> bool {
2128 conversation.is_none_or(|scope| entry.item.provenance.conversation_id == scope)
2129}
2130
2131fn cosine(a: &[f32], b: &[f32]) -> f32 {
2132 let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
2133 let norm_a = a.iter().map(|x| x * x).sum::<f32>().sqrt();
2134 let norm_b = b.iter().map(|x| x * x).sum::<f32>().sqrt();
2135 if norm_a == 0.0 || norm_b == 0.0 {
2136 0.0
2137 } else {
2138 dot / (norm_a * norm_b)
2139 }
2140}
2141
2142fn tokenize(text: &str) -> HashSet<String> {
2146 text.split(|c: char| !c.is_alphanumeric())
2147 .filter(|token| !token.is_empty())
2148 .map(str::to_lowercase)
2149 .collect()
2150}
2151
2152fn put_header(
2154 headers: &mut std::collections::BTreeMap<HeaderKey, HeaderValue>,
2155 key: &str,
2156 value: &str,
2157) -> Result<(), LaserError> {
2158 let key = HeaderKey::from_str(key)
2159 .map_err(|error| LaserError::Codec(format!("memory header key {key}: {error}")))?;
2160 let value = HeaderValue::from_str(value)
2161 .map_err(|error| LaserError::Codec(format!("memory header value: {error}")))?;
2162 headers.insert(key, value);
2163 Ok(())
2164}
2165
2166pub fn fuse_reciprocal_rank(signals: Vec<Vec<MemoryItem>>, limit: usize) -> Vec<MemoryItem> {
2172 const RRF_K: f32 = 60.0;
2173 let mut fused: Vec<MemoryItem> = Vec::new();
2174 for ranked in signals {
2175 for item in ranked {
2176 let contribution = item
2177 .signals
2178 .first()
2179 .map_or(0.0, |signal| 1.0 / (RRF_K + signal.rank as f32));
2180 match fused.iter_mut().find(|held| held.id == item.id) {
2181 Some(held) => {
2182 held.score = Some(held.score.unwrap_or(0.0) + contribution);
2183 held.signals.extend(item.signals);
2184 }
2185 None => {
2186 let mut item = item;
2187 item.score = Some(contribution);
2188 fused.push(item);
2189 }
2190 }
2191 }
2192 }
2193 fused.sort_by(|a, b| b.score.unwrap_or(0.0).total_cmp(&a.score.unwrap_or(0.0)));
2194 fused.truncate(limit);
2195 fused
2196}
2197
2198fn keyword_score(query_tokens: &HashSet<String>, body: &[u8]) -> f32 {
2202 if query_tokens.is_empty() {
2203 return 0.0;
2204 }
2205 let Ok(text) = std::str::from_utf8(body) else {
2206 return 0.0;
2207 };
2208 let body_tokens = tokenize(text);
2209 let hits = query_tokens
2210 .iter()
2211 .filter(|token| body_tokens.contains(*token))
2212 .count();
2213 hits as f32 / query_tokens.len() as f32
2214}
2215
2216fn memory_query_matches(query: &MemoryQuery, body: &[u8]) -> bool {
2217 if query.strategy == RecallStrategy::Recent {
2218 return true;
2219 }
2220 let Some(text) = query.semantic.as_deref() else {
2221 return true;
2222 };
2223 let tokens = tokenize(text);
2224 tokens.is_empty() || keyword_score(&tokens, body) > 0.0
2225}
2226
2227#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2232pub enum MemoryBackend {
2233 #[default]
2237 Auto,
2238 Log,
2240 Vector,
2244}
2245
2246pub struct MemoryTopicBuilder {
2250 laser: Laser,
2251 stream: Option<String>,
2252 topic: String,
2253 partitions: u32,
2254 ttl: Option<std::time::Duration>,
2255}
2256
2257impl MemoryTopicBuilder {
2258 #[must_use]
2261 pub fn stream(mut self, stream: impl Into<String>) -> Self {
2262 self.stream = Some(stream.into());
2263 self
2264 }
2265
2266 #[must_use]
2269 pub fn partitions(mut self, partitions: u32) -> Self {
2270 self.partitions = partitions.max(1);
2271 self
2272 }
2273
2274 #[must_use]
2279 pub fn ttl(mut self, ttl: std::time::Duration) -> Self {
2280 self.ttl = Some(ttl);
2281 self
2282 }
2283
2284 #[must_use]
2287 pub fn no_expiry(mut self) -> Self {
2288 self.ttl = None;
2289 self
2290 }
2291
2292 pub async fn build(self) -> Result<MemoryHandle, LaserError> {
2295 let stream = match &self.stream {
2296 Some(stream) => stream.clone(),
2297 None => self.laser.stream_required()?.to_owned(),
2298 };
2299 let expiry = match self.ttl {
2300 Some(ttl) => IggyExpiry::ExpireDuration(IggyDuration::from(ttl)),
2301 None => IggyExpiry::NeverExpire,
2302 };
2303 self.laser
2304 .ensure_topic_on_with(&stream, &self.topic, self.partitions, expiry)
2305 .await?;
2306 let topic = Identifier::named(&self.topic)?;
2307 Ok(MemoryHandle::Log(LogMemory::on_stream_topic(
2308 self.laser,
2309 self.stream,
2310 topic,
2311 )))
2312 }
2313}
2314
2315#[allow(clippy::large_enum_variant)]
2324pub enum MemoryHandle {
2325 Log(LogMemory),
2328 Vector(std::sync::Arc<VectorMemory<SharedEmbedder>>),
2331 Reranked {
2334 inner: Box<MemoryHandle>,
2335 reranker: SharedReranker,
2336 },
2337 Custom(std::sync::Arc<dyn DynMemory>),
2340}
2341
2342#[derive(Clone)]
2346pub struct SharedEmbedder(std::sync::Arc<dyn DynEmbedder>);
2347
2348impl Embedder for SharedEmbedder {
2349 async fn embed(&self, text: &str) -> Result<Vec<f32>, LaserError> {
2350 self.0.embed_dyn(text).await
2351 }
2352}
2353
2354trait DynEmbedder: Send + Sync {
2355 fn embed_dyn<'a>(
2356 &'a self,
2357 text: &'a str,
2358 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Vec<f32>, LaserError>> + Send + 'a>>;
2359}
2360
2361impl<E: Embedder + Sync> DynEmbedder for E {
2362 fn embed_dyn<'a>(
2363 &'a self,
2364 text: &'a str,
2365 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Vec<f32>, LaserError>> + Send + 'a>> {
2366 Box::pin(self.embed(text))
2367 }
2368}
2369
2370#[derive(Clone)]
2373pub struct SharedReranker(std::sync::Arc<dyn DynReranker>);
2374
2375impl Reranker for SharedReranker {
2376 async fn rerank(
2377 &self,
2378 query: &str,
2379 items: Vec<MemoryItem>,
2380 ) -> Result<Vec<MemoryItem>, LaserError> {
2381 self.0.rerank_dyn(query, items).await
2382 }
2383}
2384
2385trait DynReranker: Send + Sync {
2386 fn rerank_dyn<'a>(
2387 &'a self,
2388 query: &'a str,
2389 items: Vec<MemoryItem>,
2390 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Vec<MemoryItem>, LaserError>> + Send + 'a>>;
2391}
2392
2393impl<R: Reranker + Sync> DynReranker for R {
2394 fn rerank_dyn<'a>(
2395 &'a self,
2396 query: &'a str,
2397 items: Vec<MemoryItem>,
2398 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Vec<MemoryItem>, LaserError>> + Send + 'a>>
2399 {
2400 Box::pin(self.rerank(query, items))
2401 }
2402}
2403
2404impl MemoryHandle {
2405 #[must_use]
2408 pub fn backend(&self) -> &'static str {
2409 match self {
2410 Self::Log(_) => "log",
2411 Self::Vector(_) => "vector",
2412 Self::Reranked { inner, .. } => inner.backend(),
2413 Self::Custom(_) => "custom",
2414 }
2415 }
2416
2417 pub fn vector(embedder: impl Embedder + Sync + 'static) -> Self {
2421 Self::Vector(std::sync::Arc::new(VectorMemory::new(SharedEmbedder(
2422 std::sync::Arc::new(embedder),
2423 ))))
2424 }
2425
2426 #[must_use]
2431 pub fn embedder(self, embedder: impl Embedder + Sync + 'static) -> Self {
2432 self.embedder_shared(SharedEmbedder(std::sync::Arc::new(embedder)))
2433 }
2434
2435 fn embedder_shared(self, shared: SharedEmbedder) -> Self {
2436 match self {
2437 Self::Vector(memory) => Self::Vector(std::sync::Arc::new(
2438 VectorMemory::with_governance(shared, memory.governance.clone()),
2439 )),
2440 Self::Reranked { inner, reranker } => Self::Reranked {
2441 inner: Box::new(inner.embedder_shared(shared)),
2442 reranker,
2443 },
2444 Self::Log(memory) => Self::Log(memory),
2445 Self::Custom(memory) => Self::Custom(memory),
2447 }
2448 }
2449
2450 #[must_use]
2454 pub fn reranker(self, reranker: impl Reranker + Sync + 'static) -> Self {
2455 Self::Reranked {
2456 inner: Box::new(self),
2457 reranker: SharedReranker(std::sync::Arc::new(reranker)),
2458 }
2459 }
2460}
2461
2462impl Laser {
2463 pub fn memory(&self, namespace: impl Into<String>) -> MemoryHandle {
2475 self.memory_with(namespace, MemoryBackend::Auto)
2476 }
2477
2478 pub fn memory_on_topic(
2484 &self,
2485 topic: impl AsRef<str>,
2486 stream: Option<&str>,
2487 ) -> Result<MemoryHandle, LaserError> {
2488 Ok(MemoryHandle::Log(LogMemory::on_stream_topic_named(
2489 self.clone(),
2490 stream.map(str::to_owned),
2491 topic.as_ref(),
2492 )?))
2493 }
2494
2495 pub fn memory_topic(&self, topic: impl Into<String>) -> MemoryTopicBuilder {
2501 MemoryTopicBuilder {
2502 laser: self.clone(),
2503 stream: None,
2504 topic: topic.into(),
2505 partitions: 1,
2506 ttl: Some(DEFAULT_MEMORY_TOPIC_TTL),
2507 }
2508 }
2509
2510 pub fn memory_with(
2515 &self,
2516 namespace: impl Into<String>,
2517 backend: MemoryBackend,
2518 ) -> MemoryHandle {
2519 let namespace = namespace.into();
2520 match backend {
2521 MemoryBackend::Auto | MemoryBackend::Log => {
2522 MemoryHandle::Log(LogMemory::in_namespace(self.clone(), namespace))
2523 }
2524 MemoryBackend::Vector => {
2525 let _ = namespace;
2526 MemoryHandle::Vector(std::sync::Arc::new(VectorMemory::governed(
2527 self.clone(),
2528 SharedEmbedder(std::sync::Arc::new(MaybeEmbedder(None))),
2529 )))
2530 }
2531 }
2532 }
2533
2534 pub fn memory_custom(
2541 &self,
2542 backend: std::sync::Arc<dyn crate::memory::DynMemory>,
2543 ) -> MemoryHandle {
2544 MemoryHandle::Custom(backend)
2545 }
2546}
2547
2548impl MemoryHandle {
2549 async fn append_id(
2551 &self,
2552 scope: &MemoryScope,
2553 id: MemoryId,
2554 kind: MemoryKind,
2555 payload: Vec<u8>,
2556 ) -> Result<MemoryId, LaserError> {
2557 match self {
2558 MemoryHandle::Log(memory) => memory.append(scope, id, kind, payload).await,
2559 MemoryHandle::Vector(memory) => memory.append(scope, id, kind, payload).await,
2560 MemoryHandle::Reranked { inner, .. } => {
2561 append_id_boxed(inner, scope, id, kind, payload).await
2562 }
2563 MemoryHandle::Custom(memory) => memory.append(scope, id, kind, payload).await,
2566 }
2567 }
2568
2569 pub async fn set(
2577 &self,
2578 key: impl AsRef<str>,
2579 payload: impl Into<Vec<u8>>,
2580 ) -> Result<(), LaserError> {
2581 self.named_memory("set")?
2582 .set_named(key.as_ref(), payload.into())
2583 .await
2584 }
2585
2586 pub async fn fetch(&self, key: impl AsRef<str>) -> Result<Option<Vec<u8>>, LaserError> {
2588 self.named_memory("fetch")?.fetch_named(key.as_ref()).await
2589 }
2590
2591 pub async fn fetch_folded(&self, key: impl AsRef<str>) -> Result<Option<Vec<u8>>, LaserError> {
2595 self.named_memory("fetch_folded")?
2596 .fetch_named_folded(key.as_ref())
2597 .await
2598 }
2599
2600 pub async fn update(
2605 &self,
2606 key: impl AsRef<str>,
2607 patch: impl Into<Vec<u8>>,
2608 ) -> Result<(), LaserError> {
2609 self.named_memory("update")?
2610 .update_named(key.as_ref(), patch.into())
2611 .await
2612 }
2613
2614 pub async fn remove(&self, key: impl AsRef<str>) -> Result<(), LaserError> {
2616 self.named_memory("remove")?
2617 .forget_named(key.as_ref())
2618 .await
2619 }
2620
2621 fn named_memory(&self, verb: &str) -> Result<&LogMemory, LaserError> {
2624 match self {
2625 MemoryHandle::Log(memory) => Ok(memory),
2626 MemoryHandle::Reranked { inner, .. } => inner.named_memory(verb),
2627 other => Err(LaserError::unsupported(
2628 "memory",
2629 format!(
2630 "{verb}(key) is the named-item altitude and needs the durable memory handle \
2631 (this handle resolved to `{}`)",
2632 other.backend()
2633 ),
2634 )),
2635 }
2636 }
2637
2638 pub fn remember(&self, payload: impl Into<Vec<u8>>) -> RememberBuilder<'_> {
2641 RememberBuilder {
2642 handle: self,
2643 payload: payload.into(),
2644 scope: MemoryScope::default(),
2645 kind: MemoryKind::Fact,
2646 dedup: false,
2647 }
2648 }
2649
2650 pub async fn recall_folded(
2654 &self,
2655 scope: &MemoryScope,
2656 query: &MemoryQuery,
2657 ) -> Result<Vec<MemoryItem>, LaserError> {
2658 match self {
2659 MemoryHandle::Log(memory) => memory.recall_folded(scope, query).await,
2660 MemoryHandle::Vector(memory) => Memory::recall(memory.as_ref(), scope, query).await,
2661 MemoryHandle::Reranked { inner, reranker } => {
2662 let items = recall_folded_boxed(inner, scope, query).await?;
2663 match &query.semantic {
2664 Some(text) => Reranker::rerank(reranker, text, items).await,
2665 None => Ok(items),
2666 }
2667 }
2668 MemoryHandle::Custom(memory) => memory.recall(scope, query).await,
2671 }
2672 }
2673
2674 pub fn recall(&self, conversation: impl Into<Option<ConversationId>>) -> RecallBuilder<'_> {
2678 RecallBuilder {
2679 handle: self,
2680 scope: MemoryScope::builder()
2681 .maybe_conversation(conversation.into())
2682 .build(),
2683 limit: 50,
2684 semantic: None,
2685 strategy: RecallStrategy::Auto,
2686 folded: false,
2687 }
2688 }
2689
2690 pub async fn context(
2695 &self,
2696 conversation: ConversationId,
2697 token_budget: Option<usize>,
2698 ) -> Result<String, LaserError> {
2699 let items = self.recall(conversation).fetch().await?;
2700 Ok(to_context_block(&items, token_budget))
2701 }
2702
2703 pub async fn consolidate(
2708 &self,
2709 scope: &MemoryScope,
2710 max_items: usize,
2711 ) -> Result<ConsolidationReport, LaserError> {
2712 Consolidator::consolidate(&DefaultConsolidator::new(self, max_items), scope).await
2713 }
2714
2715 pub async fn consolidate_with(
2720 &self,
2721 scope: &MemoryScope,
2722 max_items: usize,
2723 summarizer: impl Summarizer + Sync,
2724 prune_summarized: bool,
2725 ) -> Result<ConsolidationReport, LaserError> {
2726 let consolidator = DefaultConsolidator::new(self, max_items).with_summarizer(summarizer);
2727 let consolidator = if prune_summarized {
2728 consolidator.prune_summarized()
2729 } else {
2730 consolidator
2731 };
2732 Consolidator::consolidate(&consolidator, scope).await
2733 }
2734
2735 pub async fn improve(
2737 &self,
2738 scope: &MemoryScope,
2739 feedback: Feedback,
2740 ) -> Result<MemoryId, LaserError> {
2741 Memory::improve(self, scope, feedback).await
2742 }
2743
2744 pub async fn forget(&self, scope: &MemoryScope, id: MemoryId) -> Result<(), LaserError> {
2746 Memory::forget(self, scope, id).await
2747 }
2748}
2749
2750impl Memory for MemoryHandle {
2751 async fn remember(
2752 &self,
2753 scope: &MemoryScope,
2754 payload: Vec<u8>,
2755 ) -> Result<MemoryId, LaserError> {
2756 if let MemoryHandle::Custom(memory) = self {
2759 return memory.remember(scope, payload).await;
2760 }
2761 self.append_id(scope, MemoryId::new(), MemoryKind::Fact, payload)
2762 .await
2763 }
2764
2765 async fn append(
2766 &self,
2767 scope: &MemoryScope,
2768 id: MemoryId,
2769 kind: MemoryKind,
2770 payload: Vec<u8>,
2771 ) -> Result<MemoryId, LaserError> {
2772 self.append_id(scope, id, kind, payload).await
2773 }
2774
2775 async fn recall(
2776 &self,
2777 scope: &MemoryScope,
2778 query: &MemoryQuery,
2779 ) -> Result<Vec<MemoryItem>, LaserError> {
2780 match self {
2781 MemoryHandle::Log(memory) => Memory::recall(memory, scope, query).await,
2782 MemoryHandle::Vector(memory) => Memory::recall(memory.as_ref(), scope, query).await,
2783 MemoryHandle::Reranked { inner, reranker } => {
2784 let items = recall_boxed(inner, scope, query).await?;
2785 match &query.semantic {
2786 Some(text) => Reranker::rerank(reranker, text, items).await,
2787 None => Ok(items),
2788 }
2789 }
2790 MemoryHandle::Custom(memory) => memory.recall(scope, query).await,
2791 }
2792 }
2793
2794 async fn improve(
2795 &self,
2796 scope: &MemoryScope,
2797 feedback: Feedback,
2798 ) -> Result<MemoryId, LaserError> {
2799 match self {
2800 MemoryHandle::Log(memory) => Memory::improve(memory, scope, feedback).await,
2801 MemoryHandle::Vector(memory) => Memory::improve(memory.as_ref(), scope, feedback).await,
2802 MemoryHandle::Reranked { inner, .. } => improve_boxed(inner, scope, feedback).await,
2803 MemoryHandle::Custom(memory) => memory.improve(scope, feedback).await,
2804 }
2805 }
2806
2807 async fn forget(&self, scope: &MemoryScope, id: MemoryId) -> Result<(), LaserError> {
2808 match self {
2809 MemoryHandle::Log(memory) => Memory::forget(memory, scope, id).await,
2810 MemoryHandle::Vector(memory) => Memory::forget(memory.as_ref(), scope, id).await,
2811 MemoryHandle::Reranked { inner, .. } => forget_boxed(inner, scope, id).await,
2812 MemoryHandle::Custom(memory) => memory.forget(scope, id).await,
2813 }
2814 }
2815}
2816
2817impl Memory for &MemoryHandle {
2818 async fn remember(
2819 &self,
2820 scope: &MemoryScope,
2821 payload: Vec<u8>,
2822 ) -> Result<MemoryId, LaserError> {
2823 Memory::remember(*self, scope, payload).await
2824 }
2825
2826 #[allow(clippy::manual_async_fn)]
2828 fn append(
2829 &self,
2830 scope: &MemoryScope,
2831 id: MemoryId,
2832 kind: MemoryKind,
2833 payload: Vec<u8>,
2834 ) -> impl std::future::Future<Output = Result<MemoryId, LaserError>> + Send
2835 where
2836 Self: Sync,
2837 {
2838 async move { Memory::append(*self, scope, id, kind, payload).await }
2839 }
2840
2841 async fn recall(
2842 &self,
2843 scope: &MemoryScope,
2844 query: &MemoryQuery,
2845 ) -> Result<Vec<MemoryItem>, LaserError> {
2846 Memory::recall(*self, scope, query).await
2847 }
2848
2849 async fn improve(
2850 &self,
2851 scope: &MemoryScope,
2852 feedback: Feedback,
2853 ) -> Result<MemoryId, LaserError> {
2854 Memory::improve(*self, scope, feedback).await
2855 }
2856
2857 async fn forget(&self, scope: &MemoryScope, id: MemoryId) -> Result<(), LaserError> {
2858 Memory::forget(*self, scope, id).await
2859 }
2860}
2861
2862#[derive(Clone)]
2865pub struct SharedConsolidator(std::sync::Arc<dyn DynConsolidator>);
2866
2867impl SharedConsolidator {
2868 pub fn new(consolidator: impl Consolidator + Sync + 'static) -> Self {
2870 Self(std::sync::Arc::new(consolidator))
2871 }
2872}
2873
2874impl Consolidator for SharedConsolidator {
2875 async fn consolidate(&self, scope: &MemoryScope) -> Result<ConsolidationReport, LaserError> {
2876 self.0.consolidate_dyn(scope).await
2877 }
2878}
2879
2880trait DynConsolidator: Send + Sync {
2881 fn consolidate_dyn<'a>(
2882 &'a self,
2883 scope: &'a MemoryScope,
2884 ) -> std::pin::Pin<Box<dyn Future<Output = Result<ConsolidationReport, LaserError>> + Send + 'a>>;
2885}
2886
2887impl<C: Consolidator + Sync> DynConsolidator for C {
2888 fn consolidate_dyn<'a>(
2889 &'a self,
2890 scope: &'a MemoryScope,
2891 ) -> std::pin::Pin<Box<dyn Future<Output = Result<ConsolidationReport, LaserError>> + Send + 'a>>
2892 {
2893 Box::pin(Consolidator::consolidate(self, scope))
2894 }
2895}
2896
2897fn append_id_boxed<'a>(
2901 inner: &'a MemoryHandle,
2902 scope: &'a MemoryScope,
2903 id: MemoryId,
2904 kind: MemoryKind,
2905 payload: Vec<u8>,
2906) -> std::pin::Pin<Box<dyn Future<Output = Result<MemoryId, LaserError>> + Send + 'a>> {
2907 Box::pin(inner.append_id(scope, id, kind, payload))
2908}
2909
2910fn recall_boxed<'a>(
2911 inner: &'a MemoryHandle,
2912 scope: &'a MemoryScope,
2913 query: &'a MemoryQuery,
2914) -> std::pin::Pin<Box<dyn Future<Output = Result<Vec<MemoryItem>, LaserError>> + Send + 'a>> {
2915 Box::pin(Memory::recall(inner, scope, query))
2916}
2917
2918fn recall_folded_boxed<'a>(
2919 inner: &'a MemoryHandle,
2920 scope: &'a MemoryScope,
2921 query: &'a MemoryQuery,
2922) -> std::pin::Pin<Box<dyn Future<Output = Result<Vec<MemoryItem>, LaserError>> + Send + 'a>> {
2923 Box::pin(inner.recall_folded(scope, query))
2924}
2925
2926fn improve_boxed<'a>(
2927 inner: &'a MemoryHandle,
2928 scope: &'a MemoryScope,
2929 feedback: Feedback,
2930) -> std::pin::Pin<Box<dyn Future<Output = Result<MemoryId, LaserError>> + Send + 'a>> {
2931 Box::pin(Memory::improve(inner, scope, feedback))
2932}
2933
2934fn forget_boxed<'a>(
2935 inner: &'a MemoryHandle,
2936 scope: &'a MemoryScope,
2937 id: MemoryId,
2938) -> std::pin::Pin<Box<dyn Future<Output = Result<(), LaserError>> + Send + 'a>> {
2939 Box::pin(Memory::forget(inner, scope, id))
2940}
2941
2942fn merge_named(current: Option<&[u8]>, patch: &[u8]) -> Result<Vec<u8>, LaserError> {
2945 let patch: serde_json::Value = serde_json::from_slice(patch)
2946 .map_err(|error| LaserError::Codec(format!("update patch is not JSON: {error}")))?;
2947 let mut base: serde_json::Value = match current {
2948 Some(payload) => serde_json::from_slice(payload)
2949 .map_err(|error| LaserError::Codec(format!("stored value is not JSON: {error}")))?,
2950 None => serde_json::Value::Null,
2951 };
2952 merge_patch(&mut base, &patch);
2953 serde_json::to_vec(&base)
2954 .map_err(|error| LaserError::Codec(format!("encode merged value: {error}")))
2955}
2956
2957fn merge_patch(base: &mut serde_json::Value, patch: &serde_json::Value) {
2960 let serde_json::Value::Object(fields) = patch else {
2961 *base = patch.clone();
2962 return;
2963 };
2964 if !base.is_object() {
2965 *base = serde_json::Value::Object(serde_json::Map::new());
2966 }
2967 let map = base.as_object_mut().expect("base was just made an object");
2968 for (key, value) in fields {
2969 if value.is_null() {
2970 map.remove(key);
2971 } else {
2972 merge_patch(
2973 map.entry(key.clone()).or_insert(serde_json::Value::Null),
2974 value,
2975 );
2976 }
2977 }
2978}
2979
2980#[derive(Clone)]
2983pub struct MaybeEmbedder(Option<SharedEmbedder>);
2984
2985impl Embedder for MaybeEmbedder {
2986 async fn embed(&self, text: &str) -> Result<Vec<f32>, LaserError> {
2987 match &self.0 {
2988 Some(embedder) => Embedder::embed(embedder, text).await,
2989 None => Err(LaserError::Config(
2990 "this memory needs an embedder for semantic recall: register one with \
2991 MemoryHandle::embedder(..)",
2992 )),
2993 }
2994 }
2995}
2996
2997pub struct RememberBuilder<'a> {
2999 handle: &'a MemoryHandle,
3000 payload: Vec<u8>,
3001 scope: MemoryScope,
3002 kind: MemoryKind,
3003 dedup: bool,
3004}
3005
3006impl RememberBuilder<'_> {
3007 #[must_use]
3009 pub fn origin(mut self, origin: SourceRef) -> Self {
3010 self.scope.origin = Some(origin);
3011 self
3012 }
3013
3014 #[must_use]
3016 pub fn producer(mut self, producer: laser_wire::graph::ProducerInfo) -> Self {
3017 self.scope.producer = Some(producer);
3018 self
3019 }
3020
3021 #[must_use]
3023 pub fn scope(mut self, conversation: ConversationId) -> Self {
3024 self.scope.conversation = Some(conversation);
3025 self
3026 }
3027
3028 #[must_use]
3030 pub fn agent(mut self, agent: AgentId) -> Self {
3031 self.scope.agent = Some(agent);
3032 self
3033 }
3034
3035 #[must_use]
3037 pub fn stream(mut self, stream: impl Into<String>) -> Self {
3038 self.scope.stream = Some(stream.into());
3039 self
3040 }
3041
3042 #[must_use]
3044 pub fn user(mut self, user: impl Into<String>) -> Self {
3045 self.scope.user = Some(user.into());
3046 self
3047 }
3048
3049 #[must_use]
3051 pub fn application(mut self, application: impl Into<String>) -> Self {
3052 self.scope.app = Some(application.into());
3053 self
3054 }
3055
3056 #[must_use]
3058 pub fn durable(mut self) -> Self {
3059 self.scope.lifetime = Lifetime::Durable;
3060 self
3061 }
3062
3063 #[must_use]
3065 pub fn kind(mut self, kind: MemoryKind) -> Self {
3066 self.kind = kind;
3067 self
3068 }
3069
3070 #[must_use]
3073 pub fn dedup(mut self) -> Self {
3074 self.dedup = true;
3075 self
3076 }
3077
3078 pub async fn send(self) -> Result<MemoryId, LaserError> {
3080 let id = if self.dedup {
3081 MemoryId::content(&self.scope, self.kind, &self.payload)
3082 } else {
3083 MemoryId::new()
3084 };
3085 self.handle
3086 .append_id(&self.scope, id, self.kind, self.payload)
3087 .await
3088 }
3089}
3090
3091pub struct RecallBuilder<'a> {
3094 handle: &'a MemoryHandle,
3095 scope: MemoryScope,
3096 limit: usize,
3097 semantic: Option<String>,
3098 strategy: RecallStrategy,
3099 folded: bool,
3100}
3101
3102impl RecallBuilder<'_> {
3103 #[must_use]
3106 pub fn semantic(mut self, text: impl Into<String>) -> Self {
3107 self.semantic = Some(text.into());
3108 self.strategy = RecallStrategy::Semantic;
3109 self
3110 }
3111
3112 #[must_use]
3114 pub fn recent(mut self) -> Self {
3115 self.strategy = RecallStrategy::Recent;
3116 self
3117 }
3118
3119 #[must_use]
3123 pub fn folded(mut self) -> Self {
3124 self.folded = true;
3125 self
3126 }
3127
3128 #[must_use]
3132 pub fn keyword(mut self, text: impl Into<String>) -> Self {
3133 self.semantic = Some(text.into());
3134 self.strategy = RecallStrategy::Keyword;
3135 self
3136 }
3137
3138 #[must_use]
3141 pub fn hybrid(mut self, text: impl Into<String>) -> Self {
3142 self.semantic = Some(text.into());
3143 self.strategy = RecallStrategy::Hybrid;
3144 self
3145 }
3146
3147 #[must_use]
3151 pub fn strategy(mut self, strategy: RecallStrategy) -> Self {
3152 self.strategy = strategy;
3153 self
3154 }
3155
3156 #[must_use]
3158 pub fn agent(mut self, agent: AgentId) -> Self {
3159 self.scope.agent = Some(agent);
3160 self
3161 }
3162
3163 #[must_use]
3165 pub fn user(mut self, user: impl Into<String>) -> Self {
3166 self.scope.user = Some(user.into());
3167 self
3168 }
3169
3170 #[must_use]
3172 pub fn application(mut self, application: impl Into<String>) -> Self {
3173 self.scope.app = Some(application.into());
3174 self
3175 }
3176
3177 #[must_use]
3179 pub fn stream(mut self, stream: impl Into<String>) -> Self {
3180 self.scope.stream = Some(stream.into());
3181 self
3182 }
3183
3184 #[must_use]
3186 pub fn limit(mut self, limit: usize) -> Self {
3187 self.limit = limit;
3188 self
3189 }
3190
3191 pub async fn block(self, token_budget: Option<usize>) -> Result<String, LaserError> {
3194 Ok(to_context_block(&self.fetch().await?, token_budget))
3195 }
3196
3197 pub async fn fetch(self) -> Result<Vec<MemoryItem>, LaserError> {
3199 let query = MemoryQuery::builder()
3200 .limit(self.limit)
3201 .maybe_semantic(self.semantic)
3202 .strategy(self.strategy)
3203 .maybe_agent(self.scope.agent.clone())
3204 .build();
3205 if self.folded {
3206 self.handle.recall_folded(&self.scope, &query).await
3207 } else {
3208 Memory::recall(self.handle, &self.scope, &query).await
3209 }
3210 }
3211}
3212
3213impl<'a> std::future::IntoFuture for RecallBuilder<'a> {
3214 type Output = Result<Vec<MemoryItem>, LaserError>;
3215 type IntoFuture =
3216 std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output> + Send + 'a>>;
3217
3218 fn into_future(self) -> Self::IntoFuture {
3219 Box::pin(self.fetch())
3220 }
3221}
3222
3223#[cfg(test)]
3224mod tests {
3225 use super::*;
3226
3227 #[test]
3228 fn given_a_default_stream_when_naming_the_memory_namespace_then_should_scope_it_to_the_record_stream()
3229 {
3230 let laser = Laser::from_client(crate::iggy::prelude::IggyClient::default())
3231 .with_default_stream("acme");
3232 let memory = LogMemory::in_namespace(laser.clone(), "notes");
3233 assert_eq!(memory.resource_namespace(), "stream:acme/notes");
3234 assert_eq!(memory.named_key("k"), "notes/k");
3235 let elsewhere =
3236 LogMemory::on_stream_topic_named(laser.clone(), Some("fleet".to_owned()), "memory")
3237 .expect("a valid topic name");
3238 assert_eq!(elsewhere.resource_namespace(), "stream:fleet/memory");
3239 assert_eq!(elsewhere.resource_stream(), Some("fleet"));
3240 let bare = LogMemory::in_namespace(
3241 laser.with_resource_naming(crate::laser::ResourceNaming::Bare),
3242 "notes",
3243 );
3244 assert_eq!(bare.resource_namespace(), "notes");
3245 assert_eq!(bare.resource_stream(), None);
3246 }
3247
3248 #[tokio::test]
3249 async fn given_a_custom_memory_backend_when_used_through_the_handle_then_should_delegate() {
3250 struct FakeMemory(std::sync::Mutex<Vec<Vec<u8>>>);
3253 impl Memory for FakeMemory {
3254 async fn remember(
3255 &self,
3256 _scope: &MemoryScope,
3257 payload: Vec<u8>,
3258 ) -> Result<MemoryId, LaserError> {
3259 self.0.lock().expect("lock").push(payload);
3260 Ok(MemoryId::new())
3261 }
3262 async fn recall(
3263 &self,
3264 _scope: &MemoryScope,
3265 _query: &MemoryQuery,
3266 ) -> Result<Vec<MemoryItem>, LaserError> {
3267 let provenance = Provenance::builder()
3268 .conversation_id(ConversationId::new())
3269 .build();
3270 Ok(self
3271 .0
3272 .lock()
3273 .expect("lock")
3274 .iter()
3275 .map(|payload| {
3276 MemoryItem::plain(MemoryId::new(), payload.clone(), provenance.clone())
3277 })
3278 .collect())
3279 }
3280 async fn improve(
3281 &self,
3282 _scope: &MemoryScope,
3283 _feedback: Feedback,
3284 ) -> Result<MemoryId, LaserError> {
3285 Ok(MemoryId::new())
3286 }
3287 async fn forget(&self, _scope: &MemoryScope, _id: MemoryId) -> Result<(), LaserError> {
3288 Ok(())
3289 }
3290 }
3291
3292 let handle = MemoryHandle::Custom(std::sync::Arc::new(FakeMemory(std::sync::Mutex::new(
3293 Vec::new(),
3294 ))));
3295 let scope = MemoryScope::default();
3296 Memory::remember(&handle, &scope, b"hi".to_vec())
3297 .await
3298 .expect("custom remember");
3299 let items = Memory::recall(&handle, &scope, &MemoryQuery::builder().limit(10).build())
3300 .await
3301 .expect("custom recall");
3302 assert_eq!(items.len(), 1);
3303 assert_eq!(items[0].payload, b"hi");
3304 }
3305
3306 #[tokio::test]
3307 async fn given_user_and_application_when_building_then_should_reach_the_backend_scope() {
3308 type Seen = std::sync::Arc<std::sync::Mutex<Vec<MemoryScope>>>;
3309 struct ScopeProbe(Seen);
3310 impl Memory for ScopeProbe {
3311 async fn remember(
3312 &self,
3313 scope: &MemoryScope,
3314 _payload: Vec<u8>,
3315 ) -> Result<MemoryId, LaserError> {
3316 self.0.lock().expect("lock").push(scope.clone());
3317 Ok(MemoryId::new())
3318 }
3319 async fn recall(
3320 &self,
3321 scope: &MemoryScope,
3322 _query: &MemoryQuery,
3323 ) -> Result<Vec<MemoryItem>, LaserError> {
3324 self.0.lock().expect("lock").push(scope.clone());
3325 let provenance = Provenance::builder()
3326 .conversation_id(ConversationId::new())
3327 .build();
3328 Ok(vec![MemoryItem::plain(
3329 MemoryId::new(),
3330 b"node-7 rotates keys monthly".to_vec(),
3331 provenance,
3332 )])
3333 }
3334 async fn improve(
3335 &self,
3336 _scope: &MemoryScope,
3337 _feedback: Feedback,
3338 ) -> Result<MemoryId, LaserError> {
3339 Ok(MemoryId::new())
3340 }
3341 async fn forget(&self, _scope: &MemoryScope, _id: MemoryId) -> Result<(), LaserError> {
3342 Ok(())
3343 }
3344 }
3345
3346 let seen = Seen::default();
3347 let handle = MemoryHandle::Custom(std::sync::Arc::new(ScopeProbe(seen.clone())));
3348 handle
3349 .remember(b"fact".to_vec())
3350 .user("operator-1")
3351 .application("fleet")
3352 .stream("fleet-memory")
3353 .send()
3354 .await
3355 .expect("the scoped remember should succeed");
3356 let block = handle
3357 .recall(ConversationId::new())
3358 .user("operator-1")
3359 .application("fleet")
3360 .stream("fleet-memory")
3361 .block(Some(100))
3362 .await
3363 .expect("the block should render");
3364 assert_eq!(block, "node-7 rotates keys monthly");
3365 let seen = seen.lock().expect("lock");
3366 assert_eq!(seen.len(), 2);
3367 for scope in seen.iter() {
3368 assert_eq!(scope.user.as_deref(), Some("operator-1"));
3369 assert_eq!(scope.app.as_deref(), Some("fleet"));
3370 assert_eq!(scope.stream.as_deref(), Some("fleet-memory"));
3371 }
3372 }
3373
3374 #[test]
3375 fn given_a_log_entry_when_encoded_then_the_wire_memory_record_decodes_it() {
3376 let id = MemoryId::new();
3380 let entry = MemoryLogEntry::Item {
3381 id,
3382 kind: MemoryKind::Message,
3383 body: b"auth is slow".to_vec(),
3384 origin: None,
3385 producer: None,
3386 };
3387 let payload = entry.encode().expect("entry encodes");
3388 let record: laser_wire::memory::MemoryRecord =
3389 laser_wire::framing::decode_named(&payload).expect("wire record decodes the SDK entry");
3390 match record {
3391 laser_wire::memory::MemoryRecord::Item {
3392 origin: None,
3393 producer: None,
3394 id: rid,
3395 kind,
3396 body,
3397 } => {
3398 assert_eq!(rid, id.to_string());
3399 assert_eq!(kind, "message");
3400 assert_eq!(body, b"auth is slow");
3401 }
3402 other => panic!("expected an Item record, got {other:?}"),
3403 }
3404 }
3405
3406 #[test]
3407 fn given_a_memory_id_when_round_tripped_through_a_string_then_should_be_equal() {
3408 let id = MemoryId::new();
3409 let parsed = id
3410 .to_string()
3411 .parse::<MemoryId>()
3412 .expect("a formatted memory id should parse");
3413 assert_eq!(parsed, id);
3414 }
3415
3416 struct MockMemory {
3419 items: std::sync::Mutex<Vec<MemoryItem>>,
3420 }
3421
3422 impl Memory for MockMemory {
3423 async fn remember(
3424 &self,
3425 _scope: &MemoryScope,
3426 payload: Vec<u8>,
3427 ) -> Result<MemoryId, LaserError> {
3428 let id = MemoryId::new();
3429 let provenance = Provenance::builder()
3430 .conversation_id(crate::types::ConversationId::new())
3431 .build();
3432 self.items
3433 .lock()
3434 .expect("lock")
3435 .push(MemoryItem::plain(id, payload, provenance));
3436 Ok(id)
3437 }
3438
3439 async fn recall(
3440 &self,
3441 _scope: &MemoryScope,
3442 query: &MemoryQuery,
3443 ) -> Result<Vec<MemoryItem>, LaserError> {
3444 Ok(self
3445 .items
3446 .lock()
3447 .expect("lock")
3448 .iter()
3449 .rev()
3450 .take(query.limit)
3451 .cloned()
3452 .collect())
3453 }
3454
3455 async fn improve(
3456 &self,
3457 _scope: &MemoryScope,
3458 _feedback: Feedback,
3459 ) -> Result<MemoryId, LaserError> {
3460 Ok(MemoryId::new())
3461 }
3462
3463 async fn forget(&self, _scope: &MemoryScope, id: MemoryId) -> Result<(), LaserError> {
3464 self.items
3465 .lock()
3466 .expect("lock")
3467 .retain(|item| item.id != id);
3468 Ok(())
3469 }
3470 }
3471
3472 #[test]
3473 fn given_more_than_max_items_when_consolidated_then_should_prune_down_to_the_cap() {
3474 let runtime = tokio::runtime::Builder::new_current_thread()
3475 .build()
3476 .expect("runtime builds");
3477 runtime.block_on(async {
3478 let mock = MockMemory {
3479 items: std::sync::Mutex::new(Vec::new()),
3480 };
3481 let scope = MemoryScope::builder().build();
3482 for i in 0..5 {
3483 Memory::remember(&mock, &scope, format!("item-{i}").into_bytes())
3484 .await
3485 .expect("remember");
3486 }
3487 let consolidator = DefaultConsolidator::new(mock, 2);
3488 let report = Consolidator::consolidate(&consolidator, &scope)
3489 .await
3490 .expect("consolidate");
3491 assert_eq!(
3492 report.pruned, 3,
3493 "five items down to a cap of two prunes three"
3494 );
3495
3496 let remaining = Memory::recall(
3497 &consolidator.memory,
3498 &scope,
3499 &MemoryQuery::builder().limit(100).build(),
3500 )
3501 .await
3502 .expect("recall");
3503 assert_eq!(remaining.len(), 2);
3504 });
3505 }
3506
3507 #[test]
3508 fn given_content_ids_when_consolidated_then_should_prune_in_arrival_order() {
3509 let runtime = tokio::runtime::Builder::new_current_thread()
3510 .build()
3511 .expect("runtime builds");
3512 runtime.block_on(async {
3513 let scope = MemoryScope::builder().build();
3514 let candidates: Vec<Vec<u8>> = (0..100)
3515 .map(|number| format!("item-{number}").into_bytes())
3516 .collect();
3517 let first = candidates
3518 .iter()
3519 .max_by_key(|body| MemoryId::content(&scope, MemoryKind::Fact, body))
3520 .expect("candidates")
3521 .clone();
3522 let first_id = MemoryId::content(&scope, MemoryKind::Fact, &first);
3523 let newest = candidates
3524 .iter()
3525 .min_by_key(|body| MemoryId::content(&scope, MemoryKind::Fact, body))
3526 .expect("candidates")
3527 .clone();
3528 let newest_id = MemoryId::content(&scope, MemoryKind::Fact, &newest);
3529 assert!(newest_id < first_id);
3530 let provenance = Provenance::builder()
3531 .conversation_id(crate::types::ConversationId::new())
3532 .build();
3533 let mock = MockMemory {
3534 items: std::sync::Mutex::new(vec![
3535 MemoryItem::plain(first_id, first, provenance.clone()),
3536 MemoryItem::plain(newest_id, newest.clone(), provenance),
3537 ]),
3538 };
3539 let consolidator = DefaultConsolidator::new(mock, 1);
3540 let report = Consolidator::consolidate(&consolidator, &scope)
3541 .await
3542 .expect("consolidate");
3543 assert_eq!(report.pruned, 1);
3544 let remaining = Memory::recall(
3545 &consolidator.memory,
3546 &scope,
3547 &MemoryQuery::builder().limit(10).build(),
3548 )
3549 .await
3550 .expect("recall");
3551 assert_eq!(remaining.len(), 1);
3552 assert_eq!(remaining[0].payload, newest);
3553 });
3554 }
3555
3556 struct StubSummarizer;
3557
3558 impl Summarizer for StubSummarizer {
3559 async fn summarize(&self, bodies: Vec<Vec<u8>>) -> Result<Vec<u8>, LaserError> {
3560 Ok(format!("summary of {}", bodies.len()).into_bytes())
3561 }
3562 }
3563
3564 #[tokio::test]
3565 async fn given_session_messages_when_consolidated_then_should_store_one_summary() {
3566 let memory = LogMemoryStub::default();
3567 let scope = MemoryScope::default();
3568 let conversation = ConversationId::new();
3569 for body in [b"turn one".to_vec(), b"turn two".to_vec()] {
3570 memory.push_in(MemoryKind::Message, body, conversation);
3571 }
3572 memory.push(MemoryKind::Fact, b"a durable fact".to_vec());
3573 let consolidator =
3574 DefaultConsolidator::new(memory.clone(), 100).with_summarizer(StubSummarizer);
3575 let report = Consolidator::consolidate(&consolidator, &scope)
3576 .await
3577 .expect("consolidates");
3578 assert_eq!(report.summarized, 2, "both session turns fold");
3579 assert_eq!(report.pruned, 0, "prune_summarized was not opted in");
3580 assert!(
3581 memory.bodies().iter().any(|body| body == b"summary of 2"),
3582 "the summary landed"
3583 );
3584 }
3585
3586 #[tokio::test]
3587 async fn given_turns_in_two_conversations_when_consolidated_then_should_store_one_summary_each()
3588 {
3589 let memory = LogMemoryStub::default();
3590 let first = ConversationId::new();
3591 let second = ConversationId::new();
3592 memory.push_in(MemoryKind::Message, b"first a".to_vec(), first);
3593 memory.push_in(MemoryKind::Message, b"second a".to_vec(), second);
3594 memory.push_in(MemoryKind::Message, b"first b".to_vec(), first);
3595 let scope = MemoryScope::builder()
3596 .agent("planner".parse().expect("valid agent"))
3597 .build();
3598 let consolidator =
3599 DefaultConsolidator::new(memory.clone(), 100).with_summarizer(StubSummarizer);
3600 let report = Consolidator::consolidate(&consolidator, &scope)
3601 .await
3602 .expect("consolidates");
3603 assert_eq!(report.summarized, 3);
3604 let summaries: Vec<_> = memory
3605 .appended()
3606 .into_iter()
3607 .map(|(scope, kind, body)| {
3608 (scope.conversation, scope.agent, scope.lifetime, kind, body)
3609 })
3610 .collect();
3611 let planner = Some("planner".parse().expect("valid agent"));
3612 assert_eq!(
3613 summaries,
3614 [
3615 (
3616 Some(first),
3617 planner.clone(),
3618 Lifetime::Durable,
3619 MemoryKind::Summary,
3620 b"summary of 2".to_vec()
3621 ),
3622 (
3623 Some(second),
3624 planner,
3625 Lifetime::Durable,
3626 MemoryKind::Summary,
3627 b"summary of 1".to_vec()
3628 ),
3629 ]
3630 );
3631 let stored = memory.items.lock().expect("stub lock").clone();
3632 let summary_conversations: Vec<_> = stored
3633 .iter()
3634 .filter(|item| item.kind == MemoryKind::Summary)
3635 .map(|item| item.provenance.conversation_id)
3636 .collect();
3637 assert_eq!(summary_conversations, [first, second]);
3638 }
3639
3640 #[tokio::test]
3641 async fn given_prune_summarized_when_consolidated_then_should_forget_the_turns() {
3642 let memory = LogMemoryStub::default();
3643 let scope = MemoryScope::default();
3644 memory.push(MemoryKind::Message, b"turn".to_vec());
3645 let consolidator = DefaultConsolidator::new(memory.clone(), 100)
3646 .with_summarizer(StubSummarizer)
3647 .prune_summarized();
3648 let report = Consolidator::consolidate(&consolidator, &scope)
3649 .await
3650 .expect("consolidates");
3651 assert_eq!(report.summarized, 1);
3652 assert_eq!(report.pruned, 1, "the folded turn is forgotten");
3653 }
3654
3655 type Appended = (MemoryScope, MemoryKind, Vec<u8>);
3657
3658 #[derive(Clone, Default)]
3662 struct LogMemoryStub {
3663 items: std::sync::Arc<std::sync::Mutex<Vec<MemoryItem>>>,
3664 appended: std::sync::Arc<std::sync::Mutex<Vec<Appended>>>,
3665 }
3666
3667 impl LogMemoryStub {
3668 fn push(&self, kind: MemoryKind, payload: Vec<u8>) {
3669 self.push_in(kind, payload, crate::types::ConversationId::new());
3670 }
3671
3672 fn push_in(&self, kind: MemoryKind, payload: Vec<u8>, conversation: ConversationId) {
3673 let provenance = Provenance::builder().conversation_id(conversation).build();
3674 let mut item = MemoryItem::plain(MemoryId::new(), payload, provenance);
3675 item.kind = kind;
3676 self.items.lock().expect("stub lock").push(item);
3677 }
3678
3679 fn appended(&self) -> Vec<Appended> {
3680 self.appended.lock().expect("stub lock").clone()
3681 }
3682
3683 fn bodies(&self) -> Vec<Vec<u8>> {
3684 self.items
3685 .lock()
3686 .expect("stub lock")
3687 .iter()
3688 .map(|item| item.payload.clone())
3689 .collect()
3690 }
3691 }
3692
3693 impl Memory for LogMemoryStub {
3694 async fn remember(
3695 &self,
3696 _scope: &MemoryScope,
3697 payload: Vec<u8>,
3698 ) -> Result<MemoryId, LaserError> {
3699 let id = MemoryId::new();
3700 let provenance = Provenance::builder()
3701 .conversation_id(crate::types::ConversationId::new())
3702 .build();
3703 self.items
3704 .lock()
3705 .expect("stub lock")
3706 .push(MemoryItem::plain(id, payload, provenance));
3707 Ok(id)
3708 }
3709
3710 async fn append(
3711 &self,
3712 scope: &MemoryScope,
3713 id: MemoryId,
3714 kind: MemoryKind,
3715 payload: Vec<u8>,
3716 ) -> Result<MemoryId, LaserError> {
3717 self.appended
3718 .lock()
3719 .expect("stub lock")
3720 .push((scope.clone(), kind, payload.clone()));
3721 self.push_in(kind, payload, scope.conversation.unwrap_or_default());
3722 Ok(id)
3723 }
3724
3725 async fn recall(
3726 &self,
3727 _scope: &MemoryScope,
3728 _query: &MemoryQuery,
3729 ) -> Result<Vec<MemoryItem>, LaserError> {
3730 Ok(self.items.lock().expect("stub lock").clone())
3731 }
3732
3733 async fn improve(
3734 &self,
3735 _scope: &MemoryScope,
3736 _feedback: Feedback,
3737 ) -> Result<MemoryId, LaserError> {
3738 Ok(MemoryId::new())
3739 }
3740
3741 async fn forget(&self, _scope: &MemoryScope, id: MemoryId) -> Result<(), LaserError> {
3742 self.items
3743 .lock()
3744 .expect("stub lock")
3745 .retain(|item| item.id != id);
3746 Ok(())
3747 }
3748 }
3749
3750 fn ranked_item(id: MemoryId, strategy: RecallStrategy, rank: usize) -> MemoryItem {
3751 let provenance = Provenance::builder()
3752 .conversation_id(crate::types::ConversationId::new())
3753 .build();
3754 let mut item = MemoryItem::plain(id, b"body".to_vec(), provenance);
3755 item.signals = vec![RecallSignal {
3756 strategy,
3757 rank,
3758 score: None,
3759 }];
3760 item
3761 }
3762
3763 #[test]
3764 fn given_two_signals_when_fused_then_should_rank_agreement_first() {
3765 let shared = MemoryId::new();
3766 let semantic_only = MemoryId::new();
3767 let keyword_only = MemoryId::new();
3768 let semantic = vec![
3769 ranked_item(semantic_only, RecallStrategy::Semantic, 0),
3770 ranked_item(shared, RecallStrategy::Semantic, 1),
3771 ];
3772 let keyword = vec![
3773 ranked_item(keyword_only, RecallStrategy::Keyword, 0),
3774 ranked_item(shared, RecallStrategy::Keyword, 1),
3775 ];
3776 let fused = fuse_reciprocal_rank(vec![semantic, keyword], 10);
3777 assert_eq!(fused[0].id, shared, "both signals agree on it");
3778 assert_eq!(fused[0].signals.len(), 2, "attribution keeps both signals");
3779 assert_eq!(fused.len(), 3);
3780 }
3781
3782 #[test]
3783 fn given_a_limit_when_fused_then_should_truncate_after_ranking() {
3784 let lists = vec![vec![
3785 ranked_item(MemoryId::new(), RecallStrategy::Semantic, 0),
3786 ranked_item(MemoryId::new(), RecallStrategy::Semantic, 1),
3787 ranked_item(MemoryId::new(), RecallStrategy::Semantic, 2),
3788 ]];
3789 assert_eq!(fuse_reciprocal_rank(lists, 2).len(), 2);
3790 }
3791
3792 fn context_item(body: &str) -> MemoryItem {
3793 let provenance = Provenance::builder()
3794 .conversation_id(crate::types::ConversationId::new())
3795 .build();
3796 MemoryItem::plain(MemoryId::new(), body.as_bytes().to_vec(), provenance)
3797 }
3798
3799 #[test]
3800 fn given_a_text_payload_when_read_then_should_return_the_text_and_json() {
3801 let item = context_item(r#"{"city":"Oslo"}"#);
3802 assert_eq!(item.text(), r#"{"city":"Oslo"}"#);
3803 let value: serde_json::Value = item.json().expect("json");
3804 assert_eq!(value["city"], "Oslo");
3805 }
3806
3807 #[test]
3808 fn given_a_non_json_payload_when_decoded_then_should_fail_with_a_codec_error() {
3809 let item = context_item("plain words");
3810 assert!(matches!(
3811 item.json::<serde_json::Value>(),
3812 Err(LaserError::Codec(_))
3813 ));
3814 }
3815
3816 #[test]
3817 fn given_no_budget_when_rendered_then_should_join_every_item() {
3818 let items = [context_item("alpha"), context_item("beta")];
3819 assert_eq!(to_context_block(&items, None), "alpha\n\nbeta");
3820 }
3821
3822 #[test]
3823 fn given_a_token_budget_when_rendered_then_should_fill_in_order_and_mark_the_overflow() {
3824 let items = [
3827 context_item("first twenty bytes!!"),
3828 context_item("second twenty bytes!"),
3829 ];
3830 let block = to_context_block(&items, Some(6));
3831 assert_eq!(
3832 block,
3833 "first twenty bytes!!\n\n[... 1 more recalled item(s) omitted ...]"
3834 );
3835 }
3836
3837 #[test]
3838 fn given_a_budget_smaller_than_the_first_item_when_rendered_then_should_keep_at_least_one() {
3839 let items = [context_item("a much longer single memory item")];
3841 let block = to_context_block(&items, Some(1));
3842 assert_eq!(block, "a much longer single memory item");
3843 }
3844
3845 fn item_entry(id: MemoryId, body: &[u8]) -> Vec<u8> {
3846 MemoryLogEntry::Item {
3847 id,
3848 kind: MemoryKind::Fact,
3849 body: body.to_vec(),
3850 origin: None,
3851 producer: None,
3852 }
3853 .encode()
3854 .expect("an item entry encodes")
3855 }
3856
3857 #[test]
3858 fn given_messages_when_absorbed_then_should_fold_items_and_apply_tombstones() {
3859 let conversation = ConversationId::new();
3860 let prov = || Provenance::builder().conversation_id(conversation).build();
3861 let mut projection = Projection::default();
3862 let id_a = MemoryId::new();
3863 let id_b = MemoryId::new();
3864 projection.absorb(&item_entry(id_a, b"a"), prov());
3865 projection.absorb(&item_entry(id_b, b"b"), prov());
3866 assert_eq!(projection.items.len(), 2, "two items folded in");
3867
3868 let tombstone = MemoryLogEntry::Forget {
3871 target: id_a,
3872 conversation: None,
3873 }
3874 .encode()
3875 .expect("a forget entry encodes");
3876 projection.absorb(&tombstone, prov());
3877 assert_eq!(projection.items.len(), 1, "tombstoned item reclaimed");
3878 assert_eq!(projection.items[0].id, id_b, "only the survivor remains");
3879 assert!(
3880 projection.is_forgotten(id_a, conversation),
3881 "first item forgotten"
3882 );
3883
3884 projection.absorb(&item_entry(id_a, b"a-again"), prov());
3886 assert_eq!(projection.items.len(), 1, "forgotten id not re-added");
3887
3888 projection.absorb(b"not an entry", prov());
3890 assert_eq!(projection.items.len(), 1, "non-entry payload dropped");
3891 }
3892
3893 #[test]
3894 fn given_a_feedback_entry_when_absorbed_then_should_accumulate_the_weight() {
3895 let conversation = ConversationId::new();
3896 let prov = || Provenance::builder().conversation_id(conversation).build();
3897 let mut projection = Projection::default();
3898 let target = MemoryId::new();
3899 let feedback = MemoryLogEntry::Feedback {
3900 target,
3901 weight: 2.5,
3902 conversation: None,
3903 }
3904 .encode()
3905 .expect("a feedback entry encodes");
3906 projection.absorb(&feedback, prov());
3907 projection.absorb(&feedback, prov());
3908 assert_eq!(projection.feedback.get(&(target, None)), Some(&5.0));
3909 assert!(projection.items.is_empty(), "feedback is not an item");
3910 }
3911
3912 #[test]
3913 fn given_scoped_tombstone_and_feedback_when_absorbed_then_should_touch_only_their_conversation()
3914 {
3915 let owner = ConversationId::new();
3916 let other = ConversationId::new();
3917 let in_owner = || Provenance::builder().conversation_id(owner).build();
3918 let in_other = || Provenance::builder().conversation_id(other).build();
3919 let mut projection = Projection::default();
3920 let id = MemoryId::new();
3921 projection.absorb(&item_entry(id, b"kept"), in_owner());
3922 let scoped = |entry: MemoryLogEntry| entry.encode().expect("entry encodes");
3923 projection.absorb(
3924 &scoped(MemoryLogEntry::Feedback {
3925 target: id,
3926 weight: 4.0,
3927 conversation: Some(other),
3928 }),
3929 in_other(),
3930 );
3931 projection.absorb(
3932 &scoped(MemoryLogEntry::Forget {
3933 target: id,
3934 conversation: Some(other),
3935 }),
3936 in_other(),
3937 );
3938 assert_eq!(
3939 projection.items.len(),
3940 1,
3941 "another conversation cannot forget it"
3942 );
3943 assert_eq!(projection.feedback_for(&projection.items[0]), None);
3944 projection.absorb(
3945 &scoped(MemoryLogEntry::Feedback {
3946 target: id,
3947 weight: 2.0,
3948 conversation: Some(owner),
3949 }),
3950 in_owner(),
3951 );
3952 assert_eq!(projection.feedback_for(&projection.items[0]), Some(2.0));
3953 projection.absorb(
3954 &scoped(MemoryLogEntry::Forget {
3955 target: id,
3956 conversation: Some(owner),
3957 }),
3958 in_owner(),
3959 );
3960 assert!(
3961 projection.items.is_empty(),
3962 "the owning conversation forgets it"
3963 );
3964 }
3965
3966 struct WordEmbedder;
3968
3969 impl Embedder for WordEmbedder {
3970 async fn embed(&self, text: &str) -> Result<Vec<f32>, LaserError> {
3971 Ok(vec![
3972 text.matches("cat").count() as f32,
3973 text.matches("dog").count() as f32,
3974 text.matches("fish").count() as f32,
3975 ])
3976 }
3977 }
3978
3979 #[tokio::test]
3982 async fn given_a_semantic_query_when_recalling_then_should_rank_by_similarity() {
3983 let memory = VectorMemory::new(WordEmbedder);
3984 let scope = MemoryScope::builder()
3985 .conversation(ConversationId::new())
3986 .build();
3987 Memory::remember(&memory, &scope, b"the cat sat".to_vec())
3988 .await
3989 .expect("remembering the cat note should succeed");
3990 Memory::remember(&memory, &scope, b"the dog ran".to_vec())
3991 .await
3992 .expect("remembering the dog note should succeed");
3993
3994 let top = Memory::recall(
3995 &memory,
3996 &scope,
3997 &MemoryQuery::builder()
3998 .semantic("cat".to_owned())
3999 .limit(1)
4000 .build(),
4001 )
4002 .await
4003 .expect("recall should succeed");
4004 assert_eq!(top.len(), 1);
4005 assert_eq!(top[0].payload.as_slice(), b"the cat sat");
4006 }
4007
4008 #[tokio::test]
4009 async fn given_a_deduped_vector_remember_when_repeated_then_should_store_one_item() {
4010 let handle = MemoryHandle::Vector(std::sync::Arc::new(VectorMemory::new(SharedEmbedder(
4011 std::sync::Arc::new(WordEmbedder),
4012 ))));
4013 let conversation = ConversationId::new();
4014 let first = handle
4015 .remember(b"stable fact".to_vec())
4016 .scope(conversation)
4017 .dedup()
4018 .send()
4019 .await
4020 .expect("first remember succeeds");
4021 let second = handle
4022 .remember(b"stable fact".to_vec())
4023 .scope(conversation)
4024 .dedup()
4025 .send()
4026 .await
4027 .expect("second remember succeeds");
4028 let recalled = handle
4029 .recall(conversation)
4030 .fetch()
4031 .await
4032 .expect("vector recall succeeds");
4033 assert_eq!(first, second);
4034 assert_eq!(recalled.len(), 1);
4035 }
4036
4037 #[test]
4038 fn given_a_memory_backend_when_defaulted_then_should_be_auto() {
4039 assert_eq!(MemoryBackend::default(), MemoryBackend::Auto);
4040 }
4041
4042 #[test]
4043 fn given_memory_kinds_when_classified_then_should_map_to_the_taxonomy() {
4044 assert_eq!(MemoryKind::Message.class(), MemoryClass::Episodic);
4045 assert_eq!(MemoryKind::Procedure.class(), MemoryClass::Procedural);
4046 assert_eq!(MemoryKind::Fact.class(), MemoryClass::Semantic);
4047 assert_eq!(MemoryKind::Summary.class(), MemoryClass::Semantic);
4048 }
4049
4050 #[tokio::test]
4051 async fn given_a_keyword_strategy_when_recalling_then_should_match_exact_terms() {
4052 let memory = VectorMemory::new(WordEmbedder);
4053 let scope = MemoryScope::builder()
4054 .conversation(ConversationId::new())
4055 .build();
4056 Memory::remember(&memory, &scope, b"alpha beta".to_vec())
4057 .await
4058 .expect("remember alpha");
4059 Memory::remember(&memory, &scope, b"gamma delta".to_vec())
4060 .await
4061 .expect("remember gamma");
4062 let top = Memory::recall(
4063 &memory,
4064 &scope,
4065 &MemoryQuery::builder()
4066 .semantic("gamma".to_owned())
4067 .strategy(RecallStrategy::Keyword)
4068 .limit(1)
4069 .build(),
4070 )
4071 .await
4072 .expect("keyword recall");
4073 assert_eq!(top.len(), 1);
4074 assert_eq!(top[0].payload.as_slice(), b"gamma delta");
4075 }
4076
4077 #[tokio::test]
4078 async fn given_a_user_scope_when_recalling_then_should_narrow_to_that_user() {
4079 let memory = VectorMemory::new(WordEmbedder);
4080 let conversation = ConversationId::new();
4081 let alice = MemoryScope::builder()
4082 .conversation(conversation)
4083 .user("alice".to_owned())
4084 .build();
4085 let bob = MemoryScope::builder()
4086 .conversation(conversation)
4087 .user("bob".to_owned())
4088 .build();
4089 Memory::remember(&memory, &alice, b"alice secret".to_vec())
4090 .await
4091 .expect("remember alice");
4092 Memory::remember(&memory, &bob, b"bob secret".to_vec())
4093 .await
4094 .expect("remember bob");
4095 let hits = Memory::recall(&memory, &alice, &MemoryQuery::builder().build())
4096 .await
4097 .expect("recall alice");
4098 assert_eq!(hits.len(), 1, "only alice's item is in scope");
4099 assert_eq!(hits[0].payload.as_slice(), b"alice secret");
4100 }
4101
4102 struct ReverseReranker;
4103
4104 impl Reranker for ReverseReranker {
4105 async fn rerank(
4106 &self,
4107 _query: &str,
4108 mut items: Vec<MemoryItem>,
4109 ) -> Result<Vec<MemoryItem>, LaserError> {
4110 items.reverse();
4111 Ok(items)
4112 }
4113 }
4114
4115 #[tokio::test]
4116 async fn given_a_reranked_memory_when_recalling_then_should_apply_the_reranker() {
4117 let inner = VectorMemory::new(WordEmbedder);
4118 let scope = MemoryScope::builder()
4119 .conversation(ConversationId::new())
4120 .build();
4121 Memory::remember(&inner, &scope, b"the cat sat".to_vec())
4122 .await
4123 .expect("remember cat");
4124 Memory::remember(&inner, &scope, b"the dog ran".to_vec())
4125 .await
4126 .expect("remember dog");
4127 let reranked = RerankedMemory::new(inner, ReverseReranker);
4128 let hits = Memory::recall(
4129 &reranked,
4130 &scope,
4131 &MemoryQuery::builder().semantic("cat".to_owned()).build(),
4132 )
4133 .await
4134 .expect("reranked recall");
4135 assert_eq!(hits.len(), 2);
4136 assert_eq!(
4137 hits[0].payload.as_slice(),
4138 b"the dog ran",
4139 "the reranker moved the dog note to the front"
4140 );
4141 }
4142
4143 #[tokio::test]
4144 async fn given_a_reranked_handle_when_recalling_folded_then_should_apply_the_reranker() {
4145 let handle = MemoryHandle::vector(WordEmbedder).reranker(ReverseReranker);
4146 let conversation = ConversationId::new();
4147 for body in ["the cat sat", "the dog ran"] {
4148 handle
4149 .remember(body)
4150 .scope(conversation)
4151 .send()
4152 .await
4153 .expect("remember");
4154 }
4155 let scope = MemoryScope::builder().conversation(conversation).build();
4156 let query = MemoryQuery::builder().semantic("cat".to_owned()).build();
4157 let hits = handle
4158 .recall_folded(&scope, &query)
4159 .await
4160 .expect("folded recall");
4161 assert_eq!(
4162 hits.iter()
4163 .map(|item| item.payload.as_slice())
4164 .collect::<Vec<_>>(),
4165 [b"the dog ran".as_slice(), b"the cat sat"],
4166 "the reranker reversed the similarity order"
4167 );
4168 }
4169
4170 #[test]
4171 fn given_the_same_content_when_addressed_then_should_produce_the_same_id() {
4172 let owner = MemoryScope::builder()
4173 .agent("planner".parse().expect("valid agent"))
4174 .lifetime(Lifetime::Durable)
4175 .build();
4176 let a = MemoryId::content(&owner, MemoryKind::Fact, b"the budget is 5000");
4177 let b = MemoryId::content(&owner, MemoryKind::Fact, b"the budget is 5000");
4178 assert_eq!(a, b, "same owner, kind, and body must dedup to one id");
4179 let other = MemoryId::content(&owner, MemoryKind::Fact, b"the budget is 6000");
4180 assert_ne!(a, other, "a different body must produce a different id");
4181 }
4182
4183 #[test]
4184 fn given_a_known_content_when_addressed_then_should_match_the_cross_sdk_id() {
4185 let agent_only = MemoryScope::builder()
4188 .agent("agent".parse().expect("valid agent"))
4189 .build();
4190 let full_owner = MemoryScope::builder()
4191 .stream("laser".to_owned())
4192 .agent("agent".parse().expect("valid agent"))
4193 .user("alice".to_owned())
4194 .app("console".to_owned())
4195 .build();
4196 let user_only = MemoryScope::builder().user("alice".to_owned()).build();
4197 let cases = [
4198 (
4199 &agent_only,
4200 MemoryKind::Fact,
4201 "63FZWE3WTSCVXAY845QK4TMVWM",
4202 0xc37f_f8e1_f359_66fa_af20_85bc_c9aa_6f94_u128,
4203 ),
4204 (
4205 &full_owner,
4206 MemoryKind::Fact,
4207 "28429K3B8MPSM8WHDQR474827P",
4208 0x4820_9331_ad14_b668_8e45_b7c1_0e44_08f6,
4209 ),
4210 (
4211 &user_only,
4212 MemoryKind::Procedure,
4213 "3E6HJGHKFDBMQ91TCJEHV28X40",
4214 0x6e34_6508_cded_5d2e_90e9_9274_7624_7480,
4215 ),
4216 ];
4217 for (owner, kind, rendered, value) in cases {
4218 let id = MemoryId::content(owner, kind, b"x");
4219 assert_eq!(id.as_u128(), value);
4220 assert_eq!(id.to_string(), rendered);
4221 }
4222 }
4223
4224 #[test]
4225 fn given_two_users_when_the_same_body_is_addressed_then_should_produce_distinct_ids() {
4226 let owner = |user: &str, application: &str| {
4227 MemoryScope::builder()
4228 .agent("agent".parse().expect("valid agent"))
4229 .user(user.to_owned())
4230 .app(application.to_owned())
4231 .build()
4232 };
4233 let alice = MemoryId::content(&owner("alice", "console"), MemoryKind::Fact, b"x");
4234 let bob = MemoryId::content(&owner("bob", "console"), MemoryKind::Fact, b"x");
4235 let support = MemoryId::content(&owner("alice", "support"), MemoryKind::Fact, b"x");
4236 assert_ne!(alice, bob, "the user is part of the owner");
4237 assert_ne!(alice, support, "the application is part of the owner");
4238 let shifted = MemoryId::content(&owner("alicecon", "sole"), MemoryKind::Fact, b"x");
4241 assert_ne!(
4242 owner("alicecon", "sole").user,
4243 owner("alice", "console").user
4244 );
4245 assert_ne!(alice, shifted);
4246 }
4247
4248 #[tokio::test]
4249 async fn given_positive_feedback_when_recalling_then_should_rank_the_target_first() {
4250 let memory = VectorMemory::new(WordEmbedder);
4251 let scope = MemoryScope::builder()
4252 .conversation(ConversationId::new())
4253 .build();
4254 let first = Memory::remember(&memory, &scope, b"the cat sat".to_vec())
4255 .await
4256 .expect("remember first");
4257 let second = Memory::remember(&memory, &scope, b"the dog ran".to_vec())
4258 .await
4259 .expect("remember second");
4260 let before = Memory::recall(&memory, &scope, &MemoryQuery::builder().build())
4262 .await
4263 .expect("recall before feedback");
4264 assert_eq!(before[0].id, second);
4265
4266 Memory::improve(&memory, &scope, Feedback::new(first, 5.0))
4268 .await
4269 .expect("improve");
4270 let after = Memory::recall(&memory, &scope, &MemoryQuery::builder().build())
4271 .await
4272 .expect("recall after feedback");
4273 assert_eq!(after[0].id, first, "the promoted item ranks first");
4274 assert_eq!(after[0].score, Some(5.0));
4275 }
4276
4277 #[test]
4278 fn given_a_named_record_when_absorbed_then_should_fold_into_point_state() {
4279 let mut projection = Projection::default();
4280 let item = laser_wire::memory::MemoryRecord::Item {
4281 origin: None,
4282 producer: None,
4283 id: "notes/current-plan".to_owned(),
4284 kind: "fact".to_owned(),
4285 body: b"ship on friday".to_vec(),
4286 };
4287 projection.absorb(
4288 &laser_wire::framing::encode_named(&item).expect("encodes"),
4289 Provenance::builder()
4290 .conversation_id(ConversationId::new())
4291 .build(),
4292 );
4293 assert_eq!(
4294 projection
4295 .named
4296 .get("notes/current-plan")
4297 .map(Vec::as_slice),
4298 Some(b"ship on friday".as_slice()),
4299 );
4300 assert!(projection.items.is_empty());
4302
4303 let forget = laser_wire::memory::MemoryRecord::Forget {
4304 target: "notes/current-plan".to_owned(),
4305 conversation: None,
4306 };
4307 projection.absorb(
4308 &laser_wire::framing::encode_named(&forget).expect("encodes"),
4309 Provenance::builder()
4310 .conversation_id(ConversationId::new())
4311 .build(),
4312 );
4313 assert!(projection.named.is_empty());
4314 }
4315
4316 #[test]
4317 fn given_a_merge_patch_when_applied_then_should_overwrite_add_and_remove_fields() {
4318 let current = br#"{"log_level":"info","replicas":3,"beta":true}"#;
4319 let patch = br#"{"log_level":"debug","region":"eu","beta":null}"#;
4320 let merged = merge_named(Some(current), patch).expect("merges");
4321 let value: serde_json::Value = serde_json::from_slice(&merged).expect("json");
4322 assert_eq!(value["log_level"], "debug"); assert_eq!(value["replicas"], 3); assert_eq!(value["region"], "eu"); assert!(value.get("beta").is_none()); }
4327
4328 #[test]
4329 fn given_no_current_value_when_merge_patched_then_should_start_from_the_patch() {
4330 let merged = merge_named(None, br#"{"log_level":"debug"}"#).expect("merges");
4331 let value: serde_json::Value = serde_json::from_slice(&merged).expect("json");
4332 assert_eq!(value["log_level"], "debug");
4333 }
4334
4335 #[tokio::test]
4336 async fn given_a_forgotten_id_when_recalling_then_should_be_excluded() {
4337 let memory = VectorMemory::new(WordEmbedder);
4338 let scope = MemoryScope::builder()
4339 .conversation(ConversationId::new())
4340 .build();
4341 let id = Memory::remember(&memory, &scope, b"cat".to_vec())
4342 .await
4343 .expect("remember should succeed");
4344 Memory::forget(&memory, &scope, id)
4345 .await
4346 .expect("forget should succeed");
4347 let all = Memory::recall(&memory, &scope, &MemoryQuery::builder().build())
4348 .await
4349 .expect("recall should succeed");
4350 assert!(all.is_empty());
4351 }
4352 #[cfg(feature = "kv")]
4353 fn scoped_kv_entry(
4354 id: u128,
4355 conversation: ConversationId,
4356 user: &str,
4357 app: &str,
4358 ) -> laser_wire::kv::KvEntry {
4359 laser_wire::kv::KvEntry {
4360 key: Ulid::from(id).to_string().into_bytes(),
4361 value: format!("item-{id}").into_bytes(),
4362 expires_at_micros: None,
4363 version: 1,
4364 scope: Some(Box::new(laser_wire::kv::MemoryRowScope {
4365 kind: Some("fact".to_owned()),
4366 user: Some(user.to_owned()),
4367 app: Some(app.to_owned()),
4368 conversation: Some(conversation.to_string()),
4369 ..Default::default()
4370 })),
4371 source: None,
4372 }
4373 }
4374
4375 #[cfg(feature = "kv")]
4376 #[tokio::test]
4377 async fn given_paged_memory_scopes_when_recalled_without_a_conversation_then_should_keep_only_newest_matching_items()
4378 {
4379 let first = ConversationId::new();
4380 let second = ConversationId::new();
4381 let scope = MemoryScope::builder()
4382 .user("reader".to_owned())
4383 .app("diagnostics".to_owned())
4384 .build();
4385 let query = MemoryQuery::builder().limit(2).build();
4386 let mut pages = std::collections::VecDeque::from([
4387 laser_wire::kv::KvPage {
4388 entries: vec![
4389 scoped_kv_entry(1, first, "reader", "diagnostics"),
4390 scoped_kv_entry(2, first, "other", "diagnostics"),
4391 ],
4392 cursor: Some(vec![2]),
4393 },
4394 laser_wire::kv::KvPage {
4395 entries: vec![
4396 scoped_kv_entry(3, second, "reader", "other"),
4397 scoped_kv_entry(4, second, "reader", "diagnostics"),
4398 scoped_kv_entry(5, first, "reader", "diagnostics"),
4399 ],
4400 cursor: None,
4401 },
4402 ]);
4403 let mut requested = Vec::new();
4404 let items = recall_kv_pages(&scope, &query, |cursor| {
4405 requested.push(cursor);
4406 std::future::ready(Ok(pages.pop_front().expect("next page")))
4407 })
4408 .await
4409 .expect("recall");
4410 assert_eq!(requested, [None, Some(vec![2])]);
4411 assert_eq!(
4412 items
4413 .iter()
4414 .map(|item| item.id.to_string())
4415 .collect::<Vec<_>>(),
4416 [Ulid::from(5u128).to_string(), Ulid::from(4u128).to_string()]
4417 );
4418 assert_eq!(items[0].provenance.conversation_id, first);
4419 assert_eq!(items[1].provenance.conversation_id, second);
4420 }
4421
4422 #[cfg(feature = "kv")]
4423 #[tokio::test]
4424 async fn given_content_ids_out_of_arrival_order_when_recalled_from_the_view_then_should_keep_the_latest_broker_time()
4425 {
4426 let conversation = ConversationId::new();
4427 let stamped = |id: u128, partition: u32, timestamp: u64| {
4428 let mut entry = scoped_kv_entry(id, conversation, "reader", "diagnostics");
4429 let scope = entry.scope.as_mut().expect("memory scope");
4430 scope.timestamp_micros = Some(timestamp);
4431 scope.source = Some(SourceRef::Message {
4432 stream: 1,
4433 topic: 2,
4434 partition,
4435 offset: 0,
4436 generation: None,
4437 conversation: None,
4438 });
4439 entry
4440 };
4441 let scope = MemoryScope::default();
4442 let query = MemoryQuery::builder().limit(2).build();
4443 let items = recall_kv_pages(&scope, &query, |_| {
4444 std::future::ready(Ok(laser_wire::kv::KvPage {
4445 entries: vec![stamped(9, 0, 10), stamped(1, 1, 30), stamped(5, 2, 20)],
4446 cursor: None,
4447 }))
4448 })
4449 .await
4450 .expect("recall");
4451 assert_eq!(
4452 items
4453 .iter()
4454 .map(|item| item.id.to_string())
4455 .collect::<Vec<_>>(),
4456 [Ulid::from(1u128).to_string(), Ulid::from(5u128).to_string()]
4457 );
4458 }
4459
4460 #[cfg(feature = "kv")]
4461 #[tokio::test]
4462 async fn given_a_repeating_memory_scan_cursor_when_recalled_then_should_refuse_to_loop() {
4463 let scope = MemoryScope::default();
4464 let query = MemoryQuery::builder().build();
4465 let result = recall_kv_pages(&scope, &query, |_| {
4466 std::future::ready(Ok(laser_wire::kv::KvPage {
4467 entries: Vec::new(),
4468 cursor: Some(vec![1]),
4469 }))
4470 })
4471 .await;
4472 assert!(matches!(result, Err(LaserError::Protocol(_))));
4473 }
4474
4475 #[test]
4476 fn given_folded_user_and_application_scopes_when_recalled_then_should_keep_the_newest_matching_records()
4477 {
4478 let conversation = ConversationId::new();
4479 let mut projection = Projection::default();
4480 for (id, user, app) in [
4481 (1u128, "reader", "diagnostics"),
4482 (2, "other", "diagnostics"),
4483 (3, "reader", "other"),
4484 (4, "reader", "diagnostics"),
4485 ] {
4486 let item = MemoryLogEntry::Item {
4487 id: MemoryId(Ulid::from(id)),
4488 kind: MemoryKind::Fact,
4489 body: format!("item-{id}").into_bytes(),
4490 origin: None,
4491 producer: None,
4492 };
4493 projection.absorb_scoped(
4494 &item.encode().expect("encode"),
4495 Provenance::builder().conversation_id(conversation).build(),
4496 StoredMemoryScope {
4497 user: Some(user.to_owned()),
4498 app: Some(app.to_owned()),
4499 },
4500 None,
4501 );
4502 }
4503 let scope = MemoryScope::builder()
4504 .conversation(conversation)
4505 .user("reader".to_owned())
4506 .app("diagnostics".to_owned())
4507 .build();
4508 let items = folded_memory_items(
4509 &projection,
4510 &scope,
4511 &MemoryQuery::builder().limit(1).build(),
4512 );
4513 assert_eq!(items.len(), 1);
4514 assert_eq!(items[0].payload, b"item-4");
4515 }
4516
4517 #[test]
4518 fn given_folded_log_memory_when_searched_then_should_exclude_unrelated_items_before_limiting() {
4519 let conversation = ConversationId::new();
4520 let mut projection = Projection::default();
4521 for (id, body) in [(1u128, b"apple".as_slice()), (2, b"orange"), (3, b"pear")] {
4522 projection.absorb(
4523 &item_entry(MemoryId(Ulid::from(id)), body),
4524 Provenance::builder().conversation_id(conversation).build(),
4525 );
4526 }
4527 for strategy in [
4528 RecallStrategy::Keyword,
4529 RecallStrategy::Semantic,
4530 RecallStrategy::Hybrid,
4531 ] {
4532 let query = MemoryQuery::builder()
4533 .semantic("apple".to_owned())
4534 .strategy(strategy)
4535 .limit(1)
4536 .build();
4537 let items = folded_memory_items(&projection, &MemoryScope::default(), &query);
4538 assert_eq!(items.len(), 1);
4539 assert_eq!(items[0].payload, b"apple");
4540 }
4541 }
4542
4543 #[cfg(feature = "kv")]
4544 #[tokio::test]
4545 async fn given_managed_log_memory_when_searched_then_should_scan_past_unrelated_newer_items() {
4546 let conversation = ConversationId::new();
4547 let mut matching = scoped_kv_entry(1, conversation, "reader", "diagnostics");
4548 matching.value = b"apple".to_vec();
4549 let mut unrelated = scoped_kv_entry(2, conversation, "reader", "diagnostics");
4550 unrelated.value = b"orange".to_vec();
4551 let scope = MemoryScope::builder().conversation(conversation).build();
4552 let query = MemoryQuery::builder()
4553 .semantic("apple".to_owned())
4554 .strategy(RecallStrategy::Keyword)
4555 .limit(1)
4556 .build();
4557 let mut page = Some(laser_wire::kv::KvPage {
4558 entries: vec![matching, unrelated],
4559 cursor: None,
4560 });
4561 let items = recall_kv_pages(&scope, &query, |_| {
4562 std::future::ready(Ok(page.take().expect("one page")))
4563 })
4564 .await
4565 .expect("recall");
4566 assert_eq!(items.len(), 1);
4567 assert_eq!(items[0].payload, b"apple");
4568 }
4569
4570 #[test]
4571 fn given_a_folded_memory_record_when_recalled_then_should_keep_its_source() {
4572 let conversation = ConversationId::new();
4573 let source = SourceRef::Message {
4574 generation: None,
4575 stream: 1,
4576 topic: 2,
4577 partition: 0,
4578 offset: 9,
4579 conversation: Some(conversation.to_string()),
4580 };
4581 let entry = MemoryLogEntry::Item {
4582 id: MemoryId(Ulid::from(7u128)),
4583 kind: MemoryKind::Fact,
4584 body: b"source fact".to_vec(),
4585 origin: None,
4586 producer: None,
4587 };
4588 let mut projection = Projection::default();
4589 projection.absorb_scoped(
4590 &entry.encode().expect("entry encodes"),
4591 Provenance::builder().conversation_id(conversation).build(),
4592 StoredMemoryScope::default(),
4593 Some(source.clone()),
4594 );
4595
4596 let items = folded_memory_items(
4597 &projection,
4598 &MemoryScope::builder().conversation(conversation).build(),
4599 &MemoryQuery::builder().build(),
4600 );
4601 assert_eq!(items[0].source, Some(source));
4602 }
4603
4604 fn projection_with_feedback_on_the_older_item() -> (Projection, MemoryId, MemoryId) {
4605 let conversation = ConversationId::new();
4606 let prov = || Provenance::builder().conversation_id(conversation).build();
4607 let mut projection = Projection::default();
4608 let older = MemoryId::new();
4609 let newer = MemoryId::new();
4610 projection.absorb(&item_entry(older, b"older"), prov());
4611 projection.absorb(&item_entry(newer, b"newer"), prov());
4612 let feedback = MemoryLogEntry::Feedback {
4613 target: older,
4614 weight: 3.0,
4615 conversation: None,
4616 }
4617 .encode()
4618 .expect("a feedback entry encodes");
4619 projection.absorb(&feedback, prov());
4620 (projection, older, newer)
4621 }
4622
4623 #[test]
4624 fn given_feedback_when_folded_recall_is_recent_then_should_keep_recency_order() {
4625 let (projection, older, newer) = projection_with_feedback_on_the_older_item();
4626 let items = folded_memory_items(
4627 &projection,
4628 &MemoryScope::default(),
4629 &MemoryQuery::builder()
4630 .strategy(RecallStrategy::Recent)
4631 .build(),
4632 );
4633 assert_eq!(
4634 items.iter().map(|item| item.id).collect::<Vec<_>>(),
4635 [newer, older]
4636 );
4637 assert!(items.iter().all(|item| item.score.is_none()));
4638 assert!(items.iter().all(|item| item.signals.is_empty()));
4639 }
4640
4641 #[test]
4642 fn given_feedback_when_folded_recall_ranks_then_should_label_the_signal_auto() {
4643 let (projection, older, _) = projection_with_feedback_on_the_older_item();
4644 let items = folded_memory_items(
4645 &projection,
4646 &MemoryScope::default(),
4647 &MemoryQuery::builder()
4648 .strategy(RecallStrategy::Semantic)
4649 .build(),
4650 );
4651 assert_eq!(items[0].id, older, "feedback promotes outside Recent");
4652 assert_eq!(
4653 items[0].signals,
4654 [RecallSignal {
4655 strategy: RecallStrategy::Auto,
4656 rank: 0,
4657 score: Some(3.0),
4658 }]
4659 );
4660 }
4661
4662 #[tokio::test]
4663 async fn given_feedback_when_vector_recall_is_recent_then_should_keep_recency_order() {
4664 let memory = VectorMemory::new(WordEmbedder);
4665 let scope = MemoryScope::default();
4666 let older = Memory::remember(&memory, &scope, b"cat".to_vec())
4667 .await
4668 .expect("remember older");
4669 let newer = Memory::remember(&memory, &scope, b"dog".to_vec())
4670 .await
4671 .expect("remember newer");
4672 Memory::improve(&memory, &scope, Feedback::new(older, 5.0))
4673 .await
4674 .expect("improve");
4675 let items = Memory::recall(
4676 &memory,
4677 &scope,
4678 &MemoryQuery::builder()
4679 .strategy(RecallStrategy::Recent)
4680 .build(),
4681 )
4682 .await
4683 .expect("recent recall");
4684 assert_eq!(
4685 items.iter().map(|item| item.id).collect::<Vec<_>>(),
4686 [newer, older]
4687 );
4688 assert!(items.iter().all(|item| item.signals.is_empty()));
4689 }
4690
4691 #[tokio::test]
4692 async fn given_a_builtin_memory_when_consolidated_then_should_append_a_durable_summary_kind() {
4693 let memory = MemoryHandle::vector(WordEmbedder);
4694 let conversation = ConversationId::new();
4695 memory
4696 .remember(b"cpu pressure".to_vec())
4697 .scope(conversation)
4698 .kind(MemoryKind::Message)
4699 .send()
4700 .await
4701 .expect("message");
4702 let scope = MemoryScope::builder().conversation(conversation).build();
4703 let consolidator = DefaultConsolidator::new(&memory, 100).with_summarizer(StubSummarizer);
4704 Consolidator::consolidate(&consolidator, &scope)
4705 .await
4706 .expect("consolidate");
4707 let MemoryHandle::Vector(store) = &memory else {
4708 panic!("vector")
4709 };
4710 let stored = store.items.lock().await;
4711 let summary = stored
4712 .iter()
4713 .find(|entry| entry.item.kind == MemoryKind::Summary)
4714 .expect("summary kind");
4715 assert_eq!(summary.scope.lifetime, Lifetime::Durable);
4716 assert_eq!(summary.item.payload, b"summary of 1");
4717 }
4718
4719 #[tokio::test]
4720 async fn given_recent_strategy_and_semantic_text_when_recalled_then_should_return_write_recency_without_embedding()
4721 {
4722 struct RecentEmbedder(std::sync::atomic::AtomicUsize);
4723 impl Embedder for RecentEmbedder {
4724 async fn embed(&self, _text: &str) -> Result<Vec<f32>, LaserError> {
4725 self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
4726 Ok(vec![1.0])
4727 }
4728 }
4729 let memory = VectorMemory::new(RecentEmbedder(std::sync::atomic::AtomicUsize::new(0)));
4730 let scope = MemoryScope::builder().stream("one".to_owned()).build();
4731 Memory::remember(&memory, &scope, b"old".to_vec())
4732 .await
4733 .expect("old");
4734 Memory::remember(&memory, &scope, b"new".to_vec())
4735 .await
4736 .expect("new");
4737 Memory::remember(
4738 &memory,
4739 &MemoryScope::builder().stream("two".to_owned()).build(),
4740 b"another stream".to_vec(),
4741 )
4742 .await
4743 .expect("other");
4744 let items = Memory::recall(
4745 &memory,
4746 &scope,
4747 &MemoryQuery::builder()
4748 .limit(1)
4749 .semantic("ignored".to_owned())
4750 .strategy(RecallStrategy::Recent)
4751 .build(),
4752 )
4753 .await
4754 .expect("recent");
4755 assert_eq!(items[0].payload, b"new");
4756 assert_eq!(
4757 memory.embedder.0.load(std::sync::atomic::Ordering::SeqCst),
4758 3
4759 );
4760 }
4761
4762 #[tokio::test]
4763 async fn given_hybrid_memory_signals_when_ranked_then_should_report_additive_scores_and_each_signal_rank()
4764 {
4765 let memory = VectorMemory::new(WordEmbedder);
4766 let scope = MemoryScope::default();
4767 let first = Memory::remember(&memory, &scope, b"auth".to_vec())
4768 .await
4769 .expect("first");
4770 let second = Memory::remember(&memory, &scope, b"auth latency".to_vec())
4771 .await
4772 .expect("second");
4773 Memory::improve(&memory, &scope, Feedback::new(second, 1.5))
4774 .await
4775 .expect("feedback");
4776 let items = Memory::recall(
4777 &memory,
4778 &scope,
4779 &MemoryQuery::builder()
4780 .semantic("auth".to_owned())
4781 .strategy(RecallStrategy::Hybrid)
4782 .build(),
4783 )
4784 .await
4785 .expect("hybrid");
4786 assert_eq!(items[0].id, second);
4787 assert_eq!(items[1].id, first);
4788 for item in &items {
4789 assert_eq!(
4790 item.score,
4791 Some(
4792 item.signals
4793 .iter()
4794 .map(|signal| signal.score.unwrap_or(0.0))
4795 .sum()
4796 )
4797 );
4798 assert!(
4799 item.signals
4800 .iter()
4801 .any(|signal| signal.strategy == RecallStrategy::Semantic)
4802 );
4803 assert!(
4804 item.signals
4805 .iter()
4806 .any(|signal| signal.strategy == RecallStrategy::Keyword)
4807 );
4808 }
4809 let semantic = items[1]
4810 .signals
4811 .iter()
4812 .find(|signal| signal.strategy == RecallStrategy::Semantic)
4813 .expect("semantic");
4814 assert_eq!(semantic.rank, 0);
4815 assert_eq!(
4816 items[0]
4817 .signals
4818 .iter()
4819 .find(|signal| signal.strategy == RecallStrategy::Auto)
4820 .expect("feedback")
4821 .rank,
4822 0
4823 );
4824 }
4825}