1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use locus_core_rs::domain::models::{AvecState, PsiRange, SttpNode};
4
5#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
6#[serde(rename_all = "snake_case")]
7pub enum FallbackPolicy {
8 Never,
9 OnEmpty,
10 Always,
11}
12
13impl Default for FallbackPolicy {
14 fn default() -> Self {
15 Self::OnEmpty
16 }
17}
18
19#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
20#[serde(rename_all = "snake_case")]
21pub enum StrictnessMode {
22 Precision,
23 Balanced,
24 Recall,
25}
26
27impl Default for StrictnessMode {
28 fn default() -> Self {
29 Self::Balanced
30 }
31}
32
33#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
34#[serde(rename_all = "snake_case")]
35pub enum MemorySortField {
36 Timestamp,
37 UpdatedAt,
38 Psi,
39 Rho,
40 Kappa,
41}
42
43impl Default for MemorySortField {
44 fn default() -> Self {
45 Self::Timestamp
46 }
47}
48
49#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
50#[serde(rename_all = "snake_case")]
51pub enum SortDirection {
52 Asc,
53 Desc,
54}
55
56impl Default for SortDirection {
57 fn default() -> Self {
58 Self::Desc
59 }
60}
61
62#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
63#[serde(rename_all = "snake_case")]
64pub enum RetrievalPath {
65 ResonanceOnly,
66 SemanticOnly,
67 Hybrid,
68 LexicalFallback,
69}
70
71#[derive(Debug, Clone, Default, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73pub struct MemoryScope {
74 pub tenant_id: Option<String>,
75 pub session_ids: Option<Vec<String>>,
76 pub tiers: Option<Vec<String>>,
77 pub from_utc: Option<DateTime<Utc>>,
78 pub to_utc: Option<DateTime<Utc>>,
79}
80
81#[derive(Debug, Clone, Default, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct MetricRange {
84 pub min: Option<f32>,
85 pub max: Option<f32>,
86}
87
88impl MetricRange {
89 pub fn contains(&self, value: f32) -> bool {
90 if let Some(min) = self.min {
91 if value < min {
92 return false;
93 }
94 }
95 if let Some(max) = self.max {
96 if value > max {
97 return false;
98 }
99 }
100 true
101 }
102}
103
104#[derive(Debug, Clone, Default, Serialize, Deserialize)]
105#[serde(rename_all = "camelCase")]
106pub struct MemoryFilter {
107 pub has_embedding: Option<bool>,
108 pub embedding_model: Option<String>,
109 pub psi: Option<MetricRange>,
110 pub rho: Option<MetricRange>,
111 pub kappa: Option<MetricRange>,
112 pub text_contains: Option<String>,
113 pub tags_contains: Option<Vec<String>>,
114 pub has_tag: Option<String>,
115 pub indexed_tags: Option<Vec<String>>,
116 pub tag_prefix: Option<String>,
117 pub has_semantic_links: Option<bool>,
118 pub link_rel: Option<String>,
119 pub link_target: Option<String>,
120 pub links_to_ref: Option<String>,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct MemoryPage {
126 pub limit: usize,
127 pub cursor: Option<String>,
128}
129
130impl Default for MemoryPage {
131 fn default() -> Self {
132 Self {
133 limit: 50,
134 cursor: None,
135 }
136 }
137}
138
139#[derive(Debug, Clone, Default, Serialize, Deserialize)]
140#[serde(rename_all = "camelCase")]
141pub struct MemorySort {
142 pub field: MemorySortField,
143 pub direction: SortDirection,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub struct MemoryScoring {
149 pub resonance_weight: f32,
150 pub semantic_weight: f32,
151 pub lexical_weight: f32,
152 pub alpha: f32,
153 pub beta: f32,
154 pub gamma: f32,
155 pub fallback_policy: FallbackPolicy,
156 pub strictness: StrictnessMode,
157}
158
159impl Default for MemoryScoring {
160 fn default() -> Self {
161 Self {
162 resonance_weight: 1.0,
163 semantic_weight: 0.0,
164 lexical_weight: 0.0,
165 alpha: 0.7,
166 beta: 0.3,
167 gamma: 0.0,
168 fallback_policy: FallbackPolicy::OnEmpty,
169 strictness: StrictnessMode::Balanced,
170 }
171 }
172}
173
174#[derive(Debug, Clone, Default, Serialize, Deserialize)]
175#[serde(rename_all = "camelCase")]
176pub struct MemoryFindRequest {
177 pub scope: MemoryScope,
178 pub filter: MemoryFilter,
179 pub page: MemoryPage,
180 pub sort: MemorySort,
181}
182
183#[derive(Debug, Clone)]
184pub struct MemoryFindResult {
185 pub nodes: Vec<SttpNode>,
186 pub retrieved: usize,
187 pub has_more: bool,
188 pub next_cursor: Option<String>,
189}
190
191#[derive(Debug, Clone, Default)]
192pub struct MemoryRecallRequest {
193 pub scope: MemoryScope,
194 pub filter: MemoryFilter,
195 pub page: MemoryPage,
196 pub scoring: MemoryScoring,
197 pub current_avec: Option<AvecState>,
198 pub query_text: Option<String>,
199 pub query_embedding: Option<Vec<f32>>,
200 pub query_tag_embedding: Option<Vec<f32>>,
201}
202
203#[derive(Debug, Clone)]
204pub struct MemoryRecallResult {
205 pub nodes: Vec<SttpNode>,
206 pub retrieved: usize,
207 pub psi_range: PsiRange,
208 pub retrieval_path: RetrievalPath,
209 pub has_more: bool,
210 pub next_cursor: Option<String>,
211}
212
213#[derive(Debug, Clone)]
214pub struct MemoryExplainRequest {
215 pub recall: MemoryRecallRequest,
216}
217
218#[derive(Debug, Clone)]
219pub struct MemoryExplainStage {
220 pub stage: String,
221 pub count: usize,
222}
223
224#[derive(Debug, Clone)]
225pub struct MemoryExplainResult {
226 pub retrieval_path: RetrievalPath,
227 pub fallback_triggered: bool,
228 pub fallback_reason: Option<String>,
229 pub stages: Vec<MemoryExplainStage>,
230 pub scoring: MemoryScoring,
231}
232
233pub const MEMORY_SCHEMA_VERSION: &str = "locus-sdk.memory.v4";
234
235#[derive(Debug, Clone, Default)]
236pub struct MemorySchemaResult {
237 pub schema_version: String,
238 pub sort_fields: Vec<String>,
239 pub filter_fields: Vec<String>,
240 pub group_by_fields: Vec<String>,
241 pub fallback_policies: Vec<String>,
242 pub strictness_modes: Vec<String>,
243 pub transform_operations: Vec<String>,
244 pub evict_operations: Vec<String>,
245 pub reflex_actions: Vec<String>,
246 pub decision_types: Vec<String>,
247}
248
249#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
250#[serde(rename_all = "snake_case")]
251pub enum MemoryGroupBy {
252 SessionId,
253 Tier,
254 EmbeddingModel,
255 DateDay,
256 SemanticTag,
257}
258
259impl Default for MemoryGroupBy {
260 fn default() -> Self {
261 Self::SessionId
262 }
263}
264
265#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
266#[serde(rename_all = "camelCase")]
267pub struct NumericStats {
268 pub min: f32,
269 pub max: f32,
270 pub average: f32,
271}
272
273#[derive(Debug, Clone, Default, Serialize, Deserialize)]
274#[serde(rename_all = "camelCase")]
275pub struct MemoryAggregateRequest {
276 pub scope: MemoryScope,
277 pub filter: MemoryFilter,
278 pub group_by: MemoryGroupBy,
279 pub max_groups: usize,
280 pub max_nodes: usize,
281}
282
283#[derive(Debug, Clone)]
284pub struct MemoryAggregateGroup {
285 pub key: String,
286 pub node_count: usize,
287 pub embedding_coverage: f32,
288 pub avg_user_avec: AvecState,
289 pub avg_model_avec: AvecState,
290 pub avg_compression_avec: Option<AvecState>,
291 pub psi_stats: NumericStats,
292 pub rho_stats: NumericStats,
293 pub kappa_stats: NumericStats,
294}
295
296#[derive(Debug, Clone, Default)]
297pub struct MemoryAggregateResult {
298 pub groups: Vec<MemoryAggregateGroup>,
299 pub total_groups: usize,
300 pub scanned_nodes: usize,
301}
302
303#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
304#[serde(rename_all = "snake_case")]
305pub enum MemoryTransformOperation {
306 EmbedBackfill,
307 ReindexEmbeddings,
308 EmbedTagBackfill,
309 ReindexTagEmbeddings,
310}
311
312impl Default for MemoryTransformOperation {
313 fn default() -> Self {
314 Self::EmbedBackfill
315 }
316}
317
318#[derive(Debug, Clone, Default, Serialize, Deserialize)]
319#[serde(rename_all = "camelCase")]
320pub struct MemoryTransformRequest {
321 pub scope: MemoryScope,
322 pub filter: MemoryFilter,
323 pub operation: MemoryTransformOperation,
324 pub dry_run: bool,
325 pub batch_size: usize,
326 pub max_nodes: usize,
327 pub provider_id: Option<String>,
328 pub model: Option<String>,
329}
330
331#[derive(Debug, Clone, Default)]
332pub struct MemoryTransformResult {
333 pub scanned: usize,
334 pub selected: usize,
335 pub updated: usize,
336 pub skipped: usize,
337 pub failed: usize,
338 pub duplicate: usize,
339 pub started_at: DateTime<Utc>,
340 pub completed_at: DateTime<Utc>,
341 pub failures: Vec<String>,
342}
343
344pub fn clamp_limit(limit: usize) -> usize {
345 limit.clamp(1, 200)
346}
347
348pub fn clamp_groups(limit: usize) -> usize {
349 limit.clamp(1, 5000)
350}
351
352pub fn clamp_nodes(limit: usize) -> usize {
353 limit.clamp(1, 50000)
354}
355
356pub fn clamp_batch_size(limit: usize) -> usize {
357 limit.clamp(1, 500)
358}