Skip to main content

llm_tool_runtime/
semantic_memory.rs

1//! Semantic-memory-facing contracts for searchable tool observations.
2
3use crate::{ToolError, ToolErrorClass};
4use async_trait::async_trait;
5use serde::{Deserialize, Serialize};
6use std::sync::Mutex;
7
8/// Serializable record written to semantic-memory for a completed or failed tool run.
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
10pub struct ToolObservationMemoryRecordV1 {
11    /// Tool descriptor name.
12    pub tool_name: String,
13    /// Stable invocation identifier for this tool call.
14    pub invocation_id: String,
15    /// Optional session identifier used to scope retrieval.
16    pub session_id: Option<String>,
17    /// Retrieval scope/namespace; authorization decisions must not be based on similarity.
18    pub scope: String,
19    /// Searchable human-readable summary of the observation.
20    pub summary: String,
21    /// Tool status, such as `success`, `error`, or `cancelled`.
22    pub status: String,
23    /// Optional RFC3339 start timestamp.
24    pub started_at: Option<String>,
25    /// Optional RFC3339 completion timestamp.
26    pub completed_at: Option<String>,
27    /// Optional durable tool receipt id.
28    pub receipt_id: Option<String>,
29}
30
31/// Semantic-memory derived-candidate receipt summary surfaced by searches.
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
33pub struct ToolObservationDerivedCandidateTraceV1 {
34    /// Candidate backend name reported by semantic-memory.
35    pub candidate_backend: String,
36    /// Codec family, for example `provekv_pool`.
37    pub codec_family: Option<String>,
38    /// Derived generation id if a generation was used.
39    pub generation_id: Option<String>,
40    /// Whether exact f32 rerank was performed or required.
41    pub exact_rerank: bool,
42    /// Whether approximate candidates influenced the pre-rerank set.
43    pub approximate: bool,
44    /// Receipted fallback reason, if any.
45    pub fallback: Option<String>,
46}
47
48/// Result of a similar-observation query, including candidate provenance.
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
50pub struct ToolObservationMemorySearchV1 {
51    /// Matching observations, already scoped by the memory implementation.
52    pub observations: Vec<ToolObservationMemoryRecordV1>,
53    /// Candidate backend receipts; candidate-only is never authorization evidence.
54    #[serde(default, skip_serializing_if = "Vec::is_empty")]
55    pub derived_candidate_receipts: Vec<ToolObservationDerivedCandidateTraceV1>,
56}
57
58/// Minimal trait for semantic-memory-backed tool observation storage/retrieval.
59#[async_trait]
60pub trait ToolObservationMemory: Send + Sync {
61    /// Store a tool observation through the configured memory substrate.
62    async fn store_tool_observation(
63        &self,
64        record: ToolObservationMemoryRecordV1,
65    ) -> Result<(), ToolError>;
66
67    /// Retrieve similar prior observations through semantic-memory; candidate-only remains non-authoritative.
68    async fn find_similar_tool_observations(
69        &self,
70        query: &str,
71        scope: &str,
72        limit: usize,
73    ) -> Result<ToolObservationMemorySearchV1, ToolError>;
74}
75
76/// Small test/demonstration implementation that mimics scoped semantic-memory retrieval.
77#[derive(Debug, Default)]
78pub struct InMemoryToolObservationMemory {
79    records: Mutex<Vec<ToolObservationMemoryRecordV1>>,
80}
81
82impl InMemoryToolObservationMemory {
83    /// Returns all records currently stored in the in-memory implementation.
84    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}