1use a3s_memory::{MemoryItem, MemoryStore, MemoryType, PrunePolicy, RelevanceConfig};
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12use std::collections::VecDeque;
13use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
14use std::sync::Arc;
15use tokio::sync::{oneshot, Notify, RwLock};
16
17#[path = "memory/maintenance.rs"]
18mod maintenance;
19#[path = "memory/semantic_refresh.rs"]
20mod semantic_refresh;
21pub use maintenance::{
22 MemoryMaintenanceCloseReport, MemoryMaintenanceContext, MemoryMaintenanceError,
23 MemoryMaintenanceHealth, MemoryMaintenanceJob, MemoryMaintenanceJobHealth,
24 MemoryMaintenanceOptions, MemoryMaintenanceOutcome, MemoryMaintenancePhase,
25 MemoryMaintenanceRuntime, ScheduledMemoryMaintenance,
26};
27pub use semantic_refresh::{
28 ScheduledSemanticRefresh, SemanticRefreshMetrics, SemanticRefreshRunMetrics,
29 SemanticRefreshRunOutcome, SEMANTIC_REFRESH_JOB_NAME, SEMANTIC_REFRESH_RECENT_RUN_LIMIT,
30};
31
32const MEMORY_STATUS_METADATA: &str = "a3s.memory.status";
33const MEMORY_STATUS_SUPERSEDED: &str = "superseded";
34
35#[derive(Debug, Clone)]
41pub struct MemoryObservation {
42 pub incoming: MemoryItem,
43 pub stored: MemoryItem,
44 pub merged: bool,
45}
46
47#[async_trait::async_trait]
53pub trait MemoryObserver: Send + Sync {
54 async fn on_memory_stored(&self, observation: MemoryObservation) -> anyhow::Result<()>;
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(rename_all = "camelCase")]
64pub struct MemoryConfig {
65 #[serde(default)]
67 pub relevance: RelevanceConfig,
68 #[serde(default = "MemoryConfig::default_max_short_term")]
70 pub max_short_term: usize,
71 #[serde(default = "MemoryConfig::default_max_working")]
73 pub max_working: usize,
74 #[serde(default)]
77 pub prune_policy: Option<PrunePolicy>,
78 #[serde(default = "MemoryConfig::default_prune_interval_secs")]
80 pub prune_interval_secs: u64,
81 #[serde(
88 default = "MemoryConfig::default_llm_extraction",
89 alias = "llm_extraction"
90 )]
91 pub llm_extraction: bool,
92 #[serde(default = "MemoryConfig::default_llm_extraction_max_items")]
94 pub llm_extraction_max_items: usize,
95 #[serde(default = "MemoryConfig::default_llm_extraction_max_input_chars")]
97 pub llm_extraction_max_input_chars: usize,
98}
99
100impl MemoryConfig {
101 fn default_max_short_term() -> usize {
102 100
103 }
104 fn default_max_working() -> usize {
105 10
106 }
107 fn default_prune_interval_secs() -> u64 {
108 3600
109 }
110 fn default_llm_extraction() -> bool {
111 true
112 }
113 fn default_llm_extraction_max_items() -> usize {
114 5
115 }
116 fn default_llm_extraction_max_input_chars() -> usize {
117 8_000
118 }
119}
120
121impl Default for MemoryConfig {
122 fn default() -> Self {
123 Self {
124 relevance: RelevanceConfig::default(),
125 max_short_term: 100,
126 max_working: 10,
127 prune_policy: None,
128 prune_interval_secs: 3600,
129 llm_extraction: true,
130 llm_extraction_max_items: 5,
131 llm_extraction_max_input_chars: 8_000,
132 }
133 }
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct MemoryStats {
143 pub long_term_count: usize,
144 pub short_term_count: usize,
145 pub working_count: usize,
146}
147
148#[derive(Clone)]
154pub struct AgentMemory {
155 pub(crate) store: Arc<dyn MemoryStore>,
157 short_term: Arc<RwLock<VecDeque<MemoryItem>>>,
159 working: Arc<RwLock<Vec<MemoryItem>>>,
161 pub(crate) max_short_term: usize,
162 pub(crate) max_working: usize,
163 pub(crate) relevance_config: RelevanceConfig,
164 pub(crate) llm_extraction: bool,
165 pub(crate) llm_extraction_max_items: usize,
166 pub(crate) llm_extraction_max_input_chars: usize,
167 extraction_queue: Arc<MemoryExtractionQueue>,
168 observers: Arc<Vec<Arc<dyn MemoryObserver>>>,
169 durable_memory: Option<crate::durable_memory::DurableMemorySession>,
170 prune_policy: Option<PrunePolicy>,
171 prune_interval: std::time::Duration,
172 maintenance_claimed: Arc<AtomicBool>,
173}
174
175#[derive(Default)]
176struct MemoryExtractionQueue {
177 state: std::sync::Mutex<MemoryExtractionQueueState>,
178 pending: AtomicUsize,
179 idle: Notify,
180}
181
182#[derive(Default)]
183struct MemoryExtractionQueueState {
184 tail: Option<oneshot::Receiver<()>>,
185}
186
187pub(crate) struct MemoryExtractionTicket {
194 predecessor: Option<oneshot::Receiver<()>>,
195 completion: Option<oneshot::Sender<()>>,
196 queue: Arc<MemoryExtractionQueue>,
197}
198
199impl MemoryExtractionTicket {
200 pub(crate) async fn wait_for_turn(&mut self) {
201 if let Some(predecessor) = self.predecessor.take() {
202 let _ = predecessor.await;
203 }
204 }
205}
206
207impl Drop for MemoryExtractionTicket {
208 fn drop(&mut self) {
209 if let Some(completion) = self.completion.take() {
210 let _ = completion.send(());
211 }
212 if self.queue.pending.fetch_sub(1, Ordering::AcqRel) == 1 {
213 self.queue.idle.notify_waiters();
214 }
215 }
216}
217
218impl std::fmt::Debug for AgentMemory {
219 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220 f.debug_struct("AgentMemory")
221 .field("max_short_term", &self.max_short_term)
222 .field("max_working", &self.max_working)
223 .field("observers", &self.observers.len())
224 .field("maintenance_configured", &self.maintenance_configured())
225 .finish()
226 }
227}
228
229impl AgentMemory {
230 pub fn new(store: Arc<dyn MemoryStore>) -> Self {
232 Self::with_config(store, MemoryConfig::default())
233 }
234
235 pub fn with_config(store: Arc<dyn MemoryStore>, config: MemoryConfig) -> Self {
240 Self::with_config_and_observers(store, config, Vec::new())
241 }
242
243 pub fn with_config_and_observers(
247 store: Arc<dyn MemoryStore>,
248 config: MemoryConfig,
249 observers: Vec<Arc<dyn MemoryObserver>>,
250 ) -> Self {
251 Self::with_config_observers_and_durable(store, config, observers, None)
252 }
253
254 pub fn with_config_observers_and_durable(
257 store: Arc<dyn MemoryStore>,
258 config: MemoryConfig,
259 observers: Vec<Arc<dyn MemoryObserver>>,
260 durable_memory: Option<crate::durable_memory::DurableMemorySession>,
261 ) -> Self {
262 Self {
263 store,
264 short_term: Arc::new(RwLock::new(VecDeque::new())),
265 working: Arc::new(RwLock::new(Vec::new())),
266 max_short_term: config.max_short_term,
267 max_working: config.max_working,
268 relevance_config: config.relevance,
269 llm_extraction: config.llm_extraction,
270 llm_extraction_max_items: config.llm_extraction_max_items,
271 llm_extraction_max_input_chars: config.llm_extraction_max_input_chars,
272 extraction_queue: Arc::new(MemoryExtractionQueue::default()),
273 observers: Arc::new(observers),
274 durable_memory,
275 prune_policy: config.prune_policy,
276 prune_interval: std::time::Duration::from_secs(config.prune_interval_secs),
277 maintenance_claimed: Arc::new(AtomicBool::new(false)),
278 }
279 }
280
281 pub(crate) fn score(&self, item: &MemoryItem, now: DateTime<Utc>) -> f32 {
282 let age_days = (now - item.timestamp).num_seconds() as f32 / 86400.0;
283 let decay = (-age_days / self.relevance_config.decay_days).exp();
284 item.importance * self.relevance_config.importance_weight
285 + decay * self.relevance_config.recency_weight
286 }
287
288 pub async fn remember(&self, item: MemoryItem) -> anyhow::Result<()> {
290 self.remember_item(item).await.map(|_| ())
291 }
292
293 pub async fn remember_item(&self, item: MemoryItem) -> anyhow::Result<MemoryItem> {
295 let incoming = item.clone();
296 let item = self.store.store_and_return(item).await?;
297 let mut short_term = self.short_term.write().await;
298 if let Some(existing) = short_term
299 .iter_mut()
300 .find(|existing| existing.id == item.id)
301 {
302 *existing = item.clone();
303 } else {
304 short_term.push_back(item.clone());
305 }
306 if short_term.len() > self.max_short_term {
307 short_term.pop_front();
308 }
309 drop(short_term);
310
311 if !self.observers.is_empty() {
312 let observation = MemoryObservation {
313 merged: item.id != incoming.id,
314 incoming,
315 stored: item.clone(),
316 };
317 for observer in self.observers.iter() {
318 if let Err(error) = observer.on_memory_stored(observation.clone()).await {
319 tracing::warn!(%error, "memory observer failed after persistence");
320 }
321 }
322 }
323 Ok(item)
324 }
325
326 pub async fn forget(&self, id: &str) -> anyhow::Result<()> {
328 self.store.delete(id).await?;
329 self.short_term.write().await.retain(|item| item.id != id);
330 self.working.write().await.retain(|item| item.id != id);
331 Ok(())
332 }
333
334 pub(crate) async fn mark_superseded(
336 &self,
337 id: &str,
338 replacement_id: &str,
339 ) -> anyhow::Result<bool> {
340 if id == replacement_id {
341 anyhow::bail!("a memory cannot supersede itself");
342 }
343 let Some(mut item) = self.store.retrieve(id).await? else {
344 return Ok(false);
345 };
346 item.metadata.insert(
347 MEMORY_STATUS_METADATA.to_string(),
348 MEMORY_STATUS_SUPERSEDED.to_string(),
349 );
350 item.metadata
351 .insert("superseded_by".to_string(), replacement_id.to_string());
352 item.metadata
353 .insert("protected".to_string(), "true".to_string());
354 if !item.tags.iter().any(|tag| tag == "superseded") {
355 item.tags.push("superseded".to_string());
356 }
357 self.store.store(item).await?;
358 self.short_term.write().await.retain(|item| item.id != id);
359 self.working.write().await.retain(|item| item.id != id);
360 Ok(true)
361 }
362
363 pub async fn remember_success(
365 &self,
366 prompt: &str,
367 tools_used: &[String],
368 result: &str,
369 ) -> anyhow::Result<()> {
370 self.remember_success_item(prompt, tools_used, result)
371 .await
372 .map(|_| ())
373 }
374
375 pub async fn remember_success_item(
377 &self,
378 prompt: &str,
379 tools_used: &[String],
380 result: &str,
381 ) -> anyhow::Result<MemoryItem> {
382 let content = format!(
383 "Success: {}\nTools: {}\nResult: {}",
384 prompt,
385 tools_used.join(", "),
386 result
387 );
388 let mut item = MemoryItem::new(content)
389 .with_importance(0.8)
390 .with_tag("success")
391 .with_tag("pattern")
392 .with_type(MemoryType::Procedural)
393 .with_metadata("prompt", prompt)
394 .with_metadata("tools", tools_used.join(","));
395 for tool in tools_used {
396 item = item.with_tag(tool.clone());
397 }
398 self.remember_item(item).await
399 }
400
401 pub async fn remember_failure(
403 &self,
404 prompt: &str,
405 error: &str,
406 attempted_tools: &[String],
407 ) -> anyhow::Result<()> {
408 self.remember_failure_item(prompt, error, attempted_tools)
409 .await
410 .map(|_| ())
411 }
412
413 pub async fn remember_failure_item(
415 &self,
416 prompt: &str,
417 error: &str,
418 attempted_tools: &[String],
419 ) -> anyhow::Result<MemoryItem> {
420 let content = format!(
421 "Failure: {}\nError: {}\nAttempted tools: {}",
422 prompt,
423 error,
424 attempted_tools.join(", ")
425 );
426 let mut item = MemoryItem::new(content)
427 .with_importance(0.9)
428 .with_tag("failure")
429 .with_tag("avoid")
430 .with_type(MemoryType::Episodic)
431 .with_metadata("prompt", prompt)
432 .with_metadata("error", error);
433 for tool in attempted_tools {
434 item = item.with_tag(tool.clone());
435 }
436 self.remember_item(item).await
437 }
438
439 pub async fn recall_similar(
441 &self,
442 prompt: &str,
443 limit: usize,
444 ) -> anyhow::Result<Vec<MemoryItem>> {
445 let items = self.store.search(prompt, recall_scan_limit(limit)).await?;
446 Ok(recallable_items(items, limit))
447 }
448
449 pub async fn recall_by_tags(
451 &self,
452 tags: &[String],
453 limit: usize,
454 ) -> anyhow::Result<Vec<MemoryItem>> {
455 let items = self
456 .store
457 .search_by_tags(tags, recall_scan_limit(limit))
458 .await?;
459 Ok(recallable_items(items, limit))
460 }
461
462 pub async fn get_recent(&self, limit: usize) -> anyhow::Result<Vec<MemoryItem>> {
464 let items = self.store.get_recent(recall_scan_limit(limit)).await?;
465 Ok(recallable_items(items, limit))
466 }
467
468 pub async fn add_to_working(&self, item: MemoryItem) -> anyhow::Result<()> {
470 let mut working = self.working.write().await;
471 working.push(item);
472 if working.len() > self.max_working {
473 let now = Utc::now();
474 working.sort_by(|a, b| {
475 self.score(b, now)
476 .partial_cmp(&self.score(a, now))
477 .unwrap_or(std::cmp::Ordering::Equal)
478 });
479 working.truncate(self.max_working);
480 }
481 Ok(())
482 }
483
484 pub async fn get_working(&self) -> Vec<MemoryItem> {
486 self.working
487 .read()
488 .await
489 .iter()
490 .filter(|item| is_recallable(item))
491 .cloned()
492 .collect()
493 }
494
495 pub async fn clear_working(&self) {
497 self.working.write().await.clear();
498 }
499
500 pub async fn get_short_term(&self) -> Vec<MemoryItem> {
502 self.short_term
503 .read()
504 .await
505 .iter()
506 .filter(|item| is_recallable(item))
507 .cloned()
508 .collect()
509 }
510
511 pub async fn clear_short_term(&self) {
513 self.short_term.write().await.clear();
514 }
515
516 pub async fn stats(&self) -> anyhow::Result<MemoryStats> {
518 Ok(MemoryStats {
519 long_term_count: self.store.count().await?,
520 short_term_count: self.short_term.read().await.len(),
521 working_count: self.working.read().await.len(),
522 })
523 }
524
525 pub fn store(&self) -> &Arc<dyn MemoryStore> {
527 &self.store
528 }
529
530 pub async fn working_count(&self) -> usize {
532 self.working.read().await.len()
533 }
534
535 pub async fn short_term_count(&self) -> usize {
537 self.short_term.read().await.len()
538 }
539
540 pub(crate) fn llm_extraction_enabled(&self) -> bool {
541 self.llm_extraction
542 }
543
544 pub(crate) fn durable_memory(&self) -> Option<&crate::durable_memory::DurableMemorySession> {
545 self.durable_memory.as_ref()
546 }
547
548 pub fn maintenance_configured(&self) -> bool {
550 self.prune_policy.is_some()
551 }
552
553 fn maintenance_prune_schedule(&self) -> Option<(PrunePolicy, std::time::Duration)> {
554 self.prune_policy
555 .clone()
556 .map(|policy| (policy, self.prune_interval))
557 }
558
559 pub(crate) fn llm_extraction_max_items(&self) -> usize {
560 self.llm_extraction_max_items
561 }
562
563 pub(crate) fn llm_extraction_max_input_chars(&self) -> usize {
564 self.llm_extraction_max_input_chars
565 }
566
567 pub(crate) fn enqueue_llm_extraction(&self) -> MemoryExtractionTicket {
568 let (completion, receiver) = oneshot::channel();
569 let predecessor = {
570 let mut state = self
571 .extraction_queue
572 .state
573 .lock()
574 .unwrap_or_else(std::sync::PoisonError::into_inner);
575 state.tail.replace(receiver)
576 };
577 self.extraction_queue.pending.fetch_add(1, Ordering::AcqRel);
578 MemoryExtractionTicket {
579 predecessor,
580 completion: Some(completion),
581 queue: Arc::clone(&self.extraction_queue),
582 }
583 }
584
585 pub(crate) async fn drain_llm_extractions(&self, timeout: std::time::Duration) -> bool {
588 let wait_until_idle = async {
589 loop {
590 let notified = self.extraction_queue.idle.notified();
591 if self.extraction_queue.pending.load(Ordering::Acquire) == 0 {
592 return;
593 }
594 notified.await;
595 }
596 };
597 tokio::time::timeout(timeout, wait_until_idle).await.is_ok()
598 }
599}
600
601pub struct MemoryContextProvider {
607 memory: AgentMemory,
608}
609
610impl MemoryContextProvider {
611 pub fn new(memory: AgentMemory) -> Self {
612 Self { memory }
613 }
614}
615
616pub(crate) fn memory_items_to_context_result(
617 provider: impl Into<String>,
618 items: Vec<MemoryItem>,
619) -> crate::context::ContextResult {
620 let mut result = crate::context::ContextResult::new(provider);
621 let items = items.into_iter().filter(is_recallable).collect::<Vec<_>>();
622 let total = items.len().max(1);
623 for (index, item) in items.into_iter().enumerate() {
624 let supersedes = relation_ids(&item, "supersedes");
625 let conflicts_with = relation_ids(&item, "conflicts_with");
626 let content = memory_context_content(&item, &supersedes, &conflicts_with);
627 let token_count = (content.len() / 4).max(1);
628 let recall_rank_score = 1.0 - (index as f32 / total as f32);
629 let relevance = (item.relevance_score() * 0.35 + recall_rank_score * 0.65).clamp(0.0, 1.0);
630 let context_item = crate::context::ContextItem::new(
631 &item.id,
632 crate::context::ContextType::Memory,
633 content,
634 )
635 .with_relevance(relevance)
636 .with_token_count(token_count)
637 .with_source(format!("memory://{}", item.id))
638 .with_metadata("memory_id", serde_json::json!(item.id))
639 .with_metadata(
640 "memory_type",
641 serde_json::json!(memory_type_label(item.memory_type)),
642 )
643 .with_metadata("tags", serde_json::json!(item.tags))
644 .with_metadata("importance", serde_json::json!(item.importance))
645 .with_provenance("long_term_memory")
646 .with_priority(0.35)
647 .with_trust(0.7)
648 .with_freshness(0.5);
649 let context_item = add_relation_metadata(context_item, "supersedes", supersedes);
650 let context_item = add_relation_metadata(context_item, "conflicts_with", conflicts_with);
651 result.add_item(context_item);
652 }
653 result
654}
655
656fn recall_scan_limit(limit: usize) -> usize {
657 limit.saturating_mul(4).max(limit)
658}
659
660fn recallable_items(items: Vec<MemoryItem>, limit: usize) -> Vec<MemoryItem> {
661 items
662 .into_iter()
663 .filter(is_recallable)
664 .take(limit)
665 .collect()
666}
667
668fn is_recallable(item: &MemoryItem) -> bool {
669 !matches!(
670 item.metadata
671 .get(MEMORY_STATUS_METADATA)
672 .map(|status| status.trim().to_ascii_lowercase()),
673 Some(status) if matches!(status.as_str(), "superseded" | "tombstoned")
674 )
675}
676
677fn relation_ids(item: &MemoryItem, key: &str) -> Vec<String> {
678 item.metadata
679 .get(key)
680 .map(|value| {
681 value
682 .split(',')
683 .map(str::trim)
684 .filter(|id| !id.is_empty())
685 .map(ToOwned::to_owned)
686 .collect()
687 })
688 .unwrap_or_default()
689}
690
691fn memory_context_content(
692 item: &MemoryItem,
693 supersedes: &[String],
694 conflicts_with: &[String],
695) -> String {
696 let mut content = item.content.clone();
697 if supersedes.is_empty() && conflicts_with.is_empty() {
698 return content;
699 }
700
701 content.push_str("\n\nMemory relations:");
702 if !supersedes.is_empty() {
703 content.push_str("\n- supersedes: ");
704 content.push_str(&relation_sources(supersedes));
705 }
706 if !conflicts_with.is_empty() {
707 content.push_str("\n- conflicts_with: ");
708 content.push_str(&relation_sources(conflicts_with));
709 }
710 content
711}
712
713fn relation_sources(ids: &[String]) -> String {
714 ids.iter()
715 .map(|id| format!("memory://{id}"))
716 .collect::<Vec<_>>()
717 .join(", ")
718}
719
720fn add_relation_metadata(
721 item: crate::context::ContextItem,
722 key: &str,
723 ids: Vec<String>,
724) -> crate::context::ContextItem {
725 if ids.is_empty() {
726 item
727 } else {
728 item.with_metadata(key, serde_json::json!(ids))
729 }
730}
731
732fn memory_type_label(memory_type: MemoryType) -> &'static str {
733 match memory_type {
734 MemoryType::Episodic => "episodic",
735 MemoryType::Semantic => "semantic",
736 MemoryType::Procedural => "procedural",
737 MemoryType::Working => "working",
738 }
739}
740
741#[async_trait::async_trait]
742impl crate::context::ContextProvider for MemoryContextProvider {
743 fn name(&self) -> &str {
744 "memory"
745 }
746
747 async fn query(
748 &self,
749 query: &crate::context::ContextQuery,
750 ) -> anyhow::Result<crate::context::ContextResult> {
751 let limit = query.max_results.min(5);
752 let items = self.memory.recall_similar(&query.query, limit).await?;
753
754 Ok(memory_items_to_context_result("memory", items))
755 }
756
757 async fn on_turn_complete(
758 &self,
759 _session_id: &str,
760 _prompt: &str,
761 _response: &str,
762 ) -> anyhow::Result<()> {
763 Ok(())
766 }
767}
768#[cfg(test)]
769#[path = "memory/tests.rs"]
770mod tests;