llm_tool_runtime/
semantic_memory.rs1use crate::{ToolError, ToolErrorClass};
4use async_trait::async_trait;
5use serde::{Deserialize, Serialize};
6use std::sync::Mutex;
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
10pub struct ToolObservationMemoryRecordV1 {
11 pub tool_name: String,
13 pub invocation_id: String,
15 pub session_id: Option<String>,
17 pub scope: String,
19 pub summary: String,
21 pub status: String,
23 pub started_at: Option<String>,
25 pub completed_at: Option<String>,
27 pub receipt_id: Option<String>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
33pub struct ToolObservationDerivedCandidateTraceV1 {
34 pub candidate_backend: String,
36 pub codec_family: Option<String>,
38 pub generation_id: Option<String>,
40 pub exact_rerank: bool,
42 pub approximate: bool,
44 pub fallback: Option<String>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
50pub struct ToolObservationMemorySearchV1 {
51 pub observations: Vec<ToolObservationMemoryRecordV1>,
53 #[serde(default, skip_serializing_if = "Vec::is_empty")]
55 pub derived_candidate_receipts: Vec<ToolObservationDerivedCandidateTraceV1>,
56}
57
58#[async_trait]
60pub trait ToolObservationMemory: Send + Sync {
61 async fn store_tool_observation(
63 &self,
64 record: ToolObservationMemoryRecordV1,
65 ) -> Result<(), ToolError>;
66
67 async fn find_similar_tool_observations(
69 &self,
70 query: &str,
71 scope: &str,
72 limit: usize,
73 ) -> Result<ToolObservationMemorySearchV1, ToolError>;
74}
75
76#[derive(Debug, Default)]
78pub struct InMemoryToolObservationMemory {
79 records: Mutex<Vec<ToolObservationMemoryRecordV1>>,
80}
81
82impl InMemoryToolObservationMemory {
83 pub fn records(&self) -> Vec<ToolObservationMemoryRecordV1> {
85 self.records
86 .lock()
87 .unwrap_or_else(|poisoned| poisoned.into_inner())
88 .clone()
89 }
90}
91
92#[async_trait]
93impl ToolObservationMemory for InMemoryToolObservationMemory {
94 async fn store_tool_observation(
95 &self,
96 record: ToolObservationMemoryRecordV1,
97 ) -> Result<(), ToolError> {
98 self.records
99 .lock()
100 .unwrap_or_else(|poisoned| poisoned.into_inner())
101 .push(record);
102 Ok(())
103 }
104
105 async fn find_similar_tool_observations(
106 &self,
107 query: &str,
108 scope: &str,
109 limit: usize,
110 ) -> Result<ToolObservationMemorySearchV1, ToolError> {
111 if limit == 0 {
112 return Err(ToolError::new(
113 ToolErrorClass::ProviderContract,
114 "limit must be greater than zero for tool observation search",
115 ));
116 }
117 let query_lc = query.to_ascii_lowercase();
118 let observations = self
119 .records()
120 .into_iter()
121 .filter(|record| record.scope == scope)
122 .filter(|record| {
123 query_lc.is_empty() || record.summary.to_ascii_lowercase().contains(&query_lc)
124 })
125 .take(limit)
126 .collect();
127 Ok(ToolObservationMemorySearchV1 {
128 observations,
129 derived_candidate_receipts: Vec::new(),
130 })
131 }
132}