Skip to main content

locus_sdk/application/
memory_composition.rs

1use std::sync::Arc;
2
3use anyhow::Result;
4use serde_json::{Map, Value};
5use locus_core_rs::domain::contracts::NodeStore;
6use locus_core_rs::domain::models::AvecState;
7
8use crate::application::memory_aggregate::MemoryAggregateService;
9use crate::application::memory_explain::MemoryExplainService;
10use crate::application::manual_compression::ManualCompressionService;
11use crate::application::memory_recall::MemoryRecallService;
12use crate::application::memory_schema::MemorySchemaService;
13use crate::application::memory_transform::MemoryTransformService;
14use crate::domain::ai::AiProviderRegistry;
15use crate::domain::compression::ManualCompressionRequest;
16use crate::domain::memory::{
17    MemoryAggregateRequest, MemoryAggregateResult, MemoryExplainRequest, MemoryExplainResult,
18    MemoryFilter, MemoryGroupBy, MemoryRecallRequest, MemoryRecallResult, MemorySchemaResult,
19    MemoryScope, MemoryTransformRequest, MemoryTransformResult, clamp_nodes,
20};
21
22#[derive(Debug, Clone)]
23pub struct MemoryRecallWithExplainResult {
24    pub recall: MemoryRecallResult,
25    pub explain: MemoryExplainResult,
26}
27
28#[derive(Debug, Clone, Default)]
29pub struct MemoryDailyRollupRequest {
30    pub scope: MemoryScope,
31    pub filter: MemoryFilter,
32    pub max_days: usize,
33    pub max_nodes: usize,
34}
35
36#[derive(Debug, Clone)]
37pub struct MemoryTransformThenRecallRequest {
38    pub transform: MemoryTransformRequest,
39    pub recall: MemoryRecallRequest,
40}
41
42#[derive(Debug, Clone)]
43pub struct MemoryTransformThenRecallResult {
44    pub transform: MemoryTransformResult,
45    pub recall: MemoryRecallResult,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum CompositeRole {
50    User,
51    Model,
52    Document,
53    Conversation,
54}
55
56impl CompositeRole {
57    fn as_str(self) -> &'static str {
58        match self {
59            Self::User => "user",
60            Self::Model => "model",
61            Self::Document => "document",
62            Self::Conversation => "conversation",
63        }
64    }
65}
66
67#[derive(Debug, Clone)]
68pub struct CompositeInputItem {
69    pub role: CompositeRole,
70    pub text: String,
71    pub avec_override: Option<AvecState>,
72    pub context: Vec<CompositeInputItem>,
73}
74
75#[derive(Debug, Clone, Default)]
76pub struct CompositeRoleAvecOverrides {
77    pub user: Option<AvecState>,
78    pub model: Option<AvecState>,
79    pub document: Option<AvecState>,
80    pub conversation: Option<AvecState>,
81}
82
83impl CompositeRoleAvecOverrides {
84    fn resolve(&self, role: CompositeRole) -> Option<AvecState> {
85        match role {
86            CompositeRole::User => self.user,
87            CompositeRole::Model => self.model,
88            CompositeRole::Document => self.document,
89            CompositeRole::Conversation => self.conversation,
90        }
91    }
92}
93
94#[derive(Debug, Clone)]
95pub struct CompositeNodeFromTextOptions {
96    pub role_avec: CompositeRoleAvecOverrides,
97    pub global_avec: Option<AvecState>,
98    pub allow_llm_avec_fallback: bool,
99    pub max_recursion_depth: usize,
100}
101
102impl Default for CompositeNodeFromTextOptions {
103    fn default() -> Self {
104        Self {
105            role_avec: CompositeRoleAvecOverrides::default(),
106            global_avec: None,
107            allow_llm_avec_fallback: false,
108            max_recursion_depth: 5,
109        }
110    }
111}
112
113#[derive(Debug, Clone, Default)]
114pub struct CompositeNodeFromTextRequest {
115    pub items: Vec<CompositeInputItem>,
116    pub options: CompositeNodeFromTextOptions,
117}
118
119#[derive(Debug, Clone, Default)]
120pub struct CompositeNodeFromTextResult {
121    pub content: Value,
122    pub resolved_avec_count: usize,
123    pub unresolved_avec_count: usize,
124    pub requires_llm_avec: bool,
125}
126
127#[derive(Debug, Clone, Default)]
128struct CompositeBuildStats {
129    resolved_avec_count: usize,
130    unresolved_avec_count: usize,
131}
132
133pub struct MemoryCompositionService {
134    store: Arc<dyn NodeStore>,
135    recall: MemoryRecallService,
136    explain: MemoryExplainService,
137    aggregate: MemoryAggregateService,
138    schema: MemorySchemaService,
139}
140
141impl MemoryCompositionService {
142    pub fn new(store: Arc<dyn NodeStore>) -> Self {
143        Self {
144            store: store.clone(),
145            recall: MemoryRecallService::new(store.clone()),
146            explain: MemoryExplainService::new(store.clone()),
147            aggregate: MemoryAggregateService::new(store),
148            schema: MemorySchemaService::new(),
149        }
150    }
151
152    pub async fn recall_with_explain(
153        &self,
154        request: &MemoryRecallRequest,
155    ) -> Result<MemoryRecallWithExplainResult> {
156        let recall = self.recall.execute(request).await?;
157        let explain = self
158            .explain
159            .execute(&MemoryExplainRequest {
160                recall: request.clone(),
161            })
162            .await?;
163
164        Ok(MemoryRecallWithExplainResult { recall, explain })
165    }
166
167    pub async fn daily_rollup(
168        &self,
169        request: &MemoryDailyRollupRequest,
170    ) -> Result<MemoryAggregateResult> {
171        let max_days = if request.max_days == 0 {
172            30
173        } else {
174            request.max_days
175        };
176        let max_nodes = clamp_nodes(if request.max_nodes == 0 {
177            5000
178        } else {
179            request.max_nodes
180        });
181
182        self.aggregate
183            .execute(&MemoryAggregateRequest {
184                scope: request.scope.clone(),
185                filter: request.filter.clone(),
186                group_by: MemoryGroupBy::DateDay,
187                max_groups: max_days,
188                max_nodes,
189            })
190            .await
191    }
192
193    pub fn capability_bundle(&self) -> MemorySchemaResult {
194        self.schema.execute()
195    }
196
197    pub async fn transform_then_recall_verify(
198        &self,
199        providers: Arc<dyn AiProviderRegistry>,
200        request: &MemoryTransformThenRecallRequest,
201    ) -> Result<MemoryTransformThenRecallResult> {
202        let transform_service = MemoryTransformService::new(self.store.clone(), providers);
203        let transform = transform_service.execute(&request.transform).await?;
204        let recall = self.recall.execute(&request.recall).await?;
205
206        Ok(MemoryTransformThenRecallResult { transform, recall })
207    }
208
209    pub fn build_content_from_text(
210        &self,
211        request: &CompositeNodeFromTextRequest,
212    ) -> Result<CompositeNodeFromTextResult> {
213        let max_depth = request.options.max_recursion_depth.clamp(1, 5);
214        let compressor = ManualCompressionService::new();
215        let mut stats = CompositeBuildStats::default();
216
217        let mut root = Map::new();
218        for (idx, item) in request.items.iter().enumerate() {
219            let key = format!("entry_{idx}(.95)");
220            let value = build_composite_entry(
221                item,
222                1,
223                max_depth,
224                &request.options,
225                &compressor,
226                &mut stats,
227            )?;
228            root.insert(key, value);
229        }
230
231        let requires_llm = stats.unresolved_avec_count > 0;
232        if requires_llm && !request.options.allow_llm_avec_fallback {
233            anyhow::bail!(
234                "unable to resolve AVEC for {} item(s); provide overrides or enable llm fallback",
235                stats.unresolved_avec_count
236            );
237        }
238
239        Ok(CompositeNodeFromTextResult {
240            content: Value::Object(root),
241            resolved_avec_count: stats.resolved_avec_count,
242            unresolved_avec_count: stats.unresolved_avec_count,
243            requires_llm_avec: requires_llm,
244        })
245    }
246}
247
248fn build_composite_entry(
249    item: &CompositeInputItem,
250    depth: usize,
251    max_depth: usize,
252    options: &CompositeNodeFromTextOptions,
253    compressor: &ManualCompressionService,
254    stats: &mut CompositeBuildStats,
255) -> Result<Value> {
256    if depth > max_depth {
257        anyhow::bail!("composite context depth exceeded max depth of {max_depth}");
258    }
259
260    let resolved_avec = item
261        .avec_override
262        .or_else(|| options.role_avec.resolve(item.role))
263        .or(options.global_avec);
264
265    if resolved_avec.is_some() {
266        stats.resolved_avec_count += 1;
267    } else {
268        stats.unresolved_avec_count += 1;
269    }
270
271    let compressed = compressor.execute(&ManualCompressionRequest {
272        text: item.text.clone(),
273        ..Default::default()
274    });
275
276    let mut entry = Map::new();
277    entry.insert(
278        "role(.99)".to_string(),
279        Value::String(item.role.as_str().to_string()),
280    );
281    entry.insert("text(.70)".to_string(), Value::String(item.text.clone()));
282    entry.insert(
283        "anchor_topic(.86)".to_string(),
284        Value::String(compressed.anchor_topic),
285    );
286    entry.insert(
287        "key_points(.82)".to_string(),
288        Value::Array(
289            compressed
290                .key_points
291                .into_iter()
292                .map(Value::String)
293                .collect(),
294        ),
295    );
296
297    if let Some(avec) = resolved_avec {
298        let mut avec_obj = Map::new();
299        avec_obj.insert("stability(.99)".to_string(), Value::from(avec.stability as f64));
300        avec_obj.insert("friction(.99)".to_string(), Value::from(avec.friction as f64));
301        avec_obj.insert("logic(.99)".to_string(), Value::from(avec.logic as f64));
302        avec_obj.insert("autonomy(.99)".to_string(), Value::from(avec.autonomy as f64));
303        avec_obj.insert("psi(.99)".to_string(), Value::from(avec.psi() as f64));
304        entry.insert("resolved_avec(.95)".to_string(), Value::Object(avec_obj));
305    }
306
307    if !item.context.is_empty() {
308        let mut children = Map::new();
309        for (idx, child) in item.context.iter().enumerate() {
310            let child_key = format!("context_{idx}(.90)");
311            children.insert(
312                child_key,
313                build_composite_entry(child, depth + 1, max_depth, options, compressor, stats)?,
314            );
315        }
316        entry.insert("context(.88)".to_string(), Value::Object(children));
317    }
318
319    Ok(Value::Object(entry))
320}
321
322#[cfg(test)]
323mod tests {
324    use std::sync::Arc;
325
326    use anyhow::Result;
327    use async_trait::async_trait;
328    use chrono::{Duration, Utc};
329    use serde_json::{Map, Value};
330    use locus_core_rs::application::validation::TreeSitterValidator;
331    use locus_core_rs::domain::contracts::NodeValidator;
332    use locus_core_rs::domain::models::{AvecState, SttpNode};
333    use locus_core_rs::parsing::SttpNodeParser;
334    use locus_core_rs::{InMemoryNodeStore, NodeStore};
335
336    use super::{
337        CompositeInputItem, CompositeNodeFromTextOptions, CompositeNodeFromTextRequest,
338        CompositeRole, MemoryCompositionService, MemoryDailyRollupRequest,
339        MemoryTransformThenRecallRequest,
340    };
341    use crate::domain::ai::{AiCapability, AiProvider, EmbedRequest, ScoreAvecRequest};
342    use crate::domain::memory::{
343        FallbackPolicy, MemoryFilter, MemoryRecallRequest, MemoryScoring, MemoryTransformOperation,
344        MemoryTransformRequest, RetrievalPath,
345    };
346    use crate::infrastructure::registry::InMemoryAiProviderRegistry;
347
348    struct MockEmbeddingProvider;
349
350    #[async_trait]
351    impl AiProvider for MockEmbeddingProvider {
352        fn provider_id(&self) -> &str {
353            "mock"
354        }
355
356        fn capabilities(&self) -> &'static [AiCapability] {
357            &[AiCapability::SemanticEmbedding]
358        }
359
360        async fn embed_semantic(&self, _request: &EmbedRequest) -> Result<Vec<f32>> {
361            Ok(vec![0.2, 0.3, 0.4])
362        }
363
364        async fn embed_avec(&self, _request: &EmbedRequest) -> Result<Vec<f32>> {
365            Ok(vec![0.2, 0.3, 0.4])
366        }
367
368        async fn score_avec(&self, _request: &ScoreAvecRequest) -> Result<AvecState> {
369            Ok(AvecState::zero())
370        }
371    }
372
373    #[tokio::test]
374    async fn recall_with_explain_returns_both_results() {
375        let store: Arc<dyn NodeStore> = Arc::new(InMemoryNodeStore::new());
376        store
377            .upsert_node_async(test_node("s-recipe", Utc::now(), "keyword in payload"))
378            .await
379            .expect("upsert should succeed");
380
381        let service = MemoryCompositionService::new(store);
382        let result = service
383            .recall_with_explain(&MemoryRecallRequest {
384                query_text: Some("keyword".to_string()),
385                ..Default::default()
386            })
387            .await
388            .expect("composed recall should succeed");
389
390        assert!(!result.explain.stages.is_empty());
391        assert!(result.recall.retrieved <= result.recall.nodes.len());
392    }
393
394    #[tokio::test]
395    async fn recall_with_explain_marks_lexical_fallback_on_empty_policy() {
396        let store: Arc<dyn NodeStore> = Arc::new(InMemoryNodeStore::new());
397        store
398            .upsert_node_async(test_node("s-fallback", Utc::now(), "payload without match"))
399            .await
400            .expect("upsert should succeed");
401
402        let service = MemoryCompositionService::new(store);
403        let result = service
404            .recall_with_explain(&MemoryRecallRequest {
405                query_text: Some("needle".to_string()),
406                filter: MemoryFilter {
407                    has_embedding: Some(true),
408                    ..Default::default()
409                },
410                scoring: MemoryScoring {
411                    fallback_policy: FallbackPolicy::OnEmpty,
412                    ..Default::default()
413                },
414                ..Default::default()
415            })
416            .await
417            .expect("composed recall should succeed");
418
419        assert_eq!(result.recall.retrieval_path, RetrievalPath::LexicalFallback);
420        assert!(result.explain.fallback_triggered);
421    }
422
423    #[tokio::test]
424    async fn daily_rollup_groups_by_day() {
425        let store: Arc<dyn NodeStore> = Arc::new(InMemoryNodeStore::new());
426        let now = Utc::now();
427        store
428            .upsert_node_async(test_node("s-rollup", now - Duration::days(1), "a"))
429            .await
430            .expect("upsert should succeed");
431        store
432            .upsert_node_async(test_node("s-rollup", now, "b"))
433            .await
434            .expect("upsert should succeed");
435
436        let service = MemoryCompositionService::new(store);
437        let result = service
438            .daily_rollup(&MemoryDailyRollupRequest {
439                max_days: 10,
440                max_nodes: 100,
441                ..Default::default()
442            })
443            .await
444            .expect("daily rollup should succeed");
445
446        assert!(result.total_groups >= 2);
447    }
448
449    #[test]
450    fn capability_bundle_exposes_schema() {
451        let store: Arc<dyn NodeStore> = Arc::new(InMemoryNodeStore::new());
452        let service = MemoryCompositionService::new(store);
453        let schema = service.capability_bundle();
454
455        assert_eq!(schema.schema_version, "locus-sdk.memory.v1");
456        assert!(schema
457            .transform_operations
458            .contains(&"embed_backfill".to_string()));
459    }
460
461    #[tokio::test]
462    async fn transform_then_recall_verify_returns_both_sides() {
463        let store: Arc<dyn NodeStore> = Arc::new(InMemoryNodeStore::new());
464        store
465            .upsert_node_async(test_node("s-verify", Utc::now(), "verification payload"))
466            .await
467            .expect("upsert should succeed");
468
469        let service = MemoryCompositionService::new(store.clone());
470
471        let mut providers = InMemoryAiProviderRegistry::new();
472        providers.register(MockEmbeddingProvider);
473
474        let result = service
475            .transform_then_recall_verify(
476                Arc::new(providers),
477                &MemoryTransformThenRecallRequest {
478                    transform: MemoryTransformRequest {
479                        operation: MemoryTransformOperation::EmbedBackfill,
480                        dry_run: false,
481                        max_nodes: 100,
482                        batch_size: 10,
483                        ..Default::default()
484                    },
485                    recall: MemoryRecallRequest {
486                        query_text: Some("verification".to_string()),
487                        ..Default::default()
488                    },
489                },
490            )
491            .await
492            .expect("transform then recall should succeed");
493
494        assert_eq!(result.transform.failed, 0);
495        assert_eq!(result.transform.updated, 1);
496        assert!(result.recall.retrieved <= result.recall.nodes.len());
497    }
498
499    #[test]
500    fn build_content_from_text_resolves_avec_from_role_then_global() {
501        let store: Arc<dyn NodeStore> = Arc::new(InMemoryNodeStore::new());
502        let service = MemoryCompositionService::new(store);
503
504        let role_state = AvecState {
505            stability: 0.6,
506            friction: 0.2,
507            logic: 0.8,
508            autonomy: 0.7,
509        };
510        let global_state = AvecState {
511            stability: 0.4,
512            friction: 0.3,
513            logic: 0.6,
514            autonomy: 0.5,
515        };
516
517        let result = service
518            .build_content_from_text(&CompositeNodeFromTextRequest {
519                items: vec![
520                    CompositeInputItem {
521                        role: CompositeRole::User,
522                        text: "policy retrieval stability".to_string(),
523                        avec_override: None,
524                        context: Vec::new(),
525                    },
526                    CompositeInputItem {
527                        role: CompositeRole::Document,
528                        text: "technical writeup migration".to_string(),
529                        avec_override: None,
530                        context: Vec::new(),
531                    },
532                ],
533                options: CompositeNodeFromTextOptions {
534                    role_avec: super::CompositeRoleAvecOverrides {
535                        user: Some(role_state),
536                        ..Default::default()
537                    },
538                    global_avec: Some(global_state),
539                    ..Default::default()
540                },
541            })
542            .expect("composite build should succeed");
543
544        assert_eq!(result.resolved_avec_count, 2);
545        assert_eq!(result.unresolved_avec_count, 0);
546        assert!(!result.requires_llm_avec);
547    }
548
549    #[test]
550    fn build_content_from_text_fails_without_resolved_avec_when_llm_disabled() {
551        let store: Arc<dyn NodeStore> = Arc::new(InMemoryNodeStore::new());
552        let service = MemoryCompositionService::new(store);
553
554        let err = service
555            .build_content_from_text(&CompositeNodeFromTextRequest {
556                items: vec![CompositeInputItem {
557                    role: CompositeRole::Conversation,
558                    text: "user asked then model replied".to_string(),
559                    avec_override: None,
560                    context: Vec::new(),
561                }],
562                options: CompositeNodeFromTextOptions {
563                    allow_llm_avec_fallback: false,
564                    ..Default::default()
565                },
566            })
567            .expect_err("missing avec should fail when llm fallback is disabled");
568
569        assert!(err.to_string().contains("unable to resolve AVEC"));
570    }
571
572    #[test]
573    fn build_content_from_text_enforces_depth_limit() {
574        let store: Arc<dyn NodeStore> = Arc::new(InMemoryNodeStore::new());
575        let service = MemoryCompositionService::new(store);
576
577        let leaf = CompositeInputItem {
578            role: CompositeRole::User,
579            text: "leaf".to_string(),
580            avec_override: Some(AvecState::zero()),
581            context: Vec::new(),
582        };
583
584        let depth2 = CompositeInputItem {
585            role: CompositeRole::User,
586            text: "depth2".to_string(),
587            avec_override: Some(AvecState::zero()),
588            context: vec![leaf],
589        };
590
591        let depth1 = CompositeInputItem {
592            role: CompositeRole::User,
593            text: "depth1".to_string(),
594            avec_override: Some(AvecState::zero()),
595            context: vec![depth2],
596        };
597
598        let err = service
599            .build_content_from_text(&CompositeNodeFromTextRequest {
600                items: vec![depth1],
601                options: CompositeNodeFromTextOptions {
602                    max_recursion_depth: 2,
603                    ..Default::default()
604                },
605            })
606            .expect_err("depth overflow should fail");
607
608        assert!(err.to_string().contains("depth exceeded"));
609    }
610
611    #[test]
612    fn composite_content_parses_and_validates_under_strict_profile() {
613        let store: Arc<dyn NodeStore> = Arc::new(InMemoryNodeStore::new());
614        let service = MemoryCompositionService::new(store);
615
616        let request = CompositeNodeFromTextRequest {
617            items: vec![CompositeInputItem {
618                role: CompositeRole::Conversation,
619                text: "user asked for deterministic recall and model proposed fallback policy".to_string(),
620                avec_override: Some(AvecState {
621                    stability: 0.8,
622                    friction: 0.2,
623                    logic: 0.85,
624                    autonomy: 0.75,
625                }),
626                context: vec![CompositeInputItem {
627                    role: CompositeRole::Document,
628                    text: "system notes include lexical fallback and ranked retrieval".to_string(),
629                    avec_override: Some(AvecState {
630                        stability: 0.7,
631                        friction: 0.3,
632                        logic: 0.8,
633                        autonomy: 0.7,
634                    }),
635                    context: Vec::new(),
636                }],
637            }],
638            options: CompositeNodeFromTextOptions {
639                allow_llm_avec_fallback: false,
640                ..Default::default()
641            },
642        };
643
644        let result = service
645            .build_content_from_text(&request)
646            .expect("composite content build should succeed");
647
648        let raw_node = render_sttp_node("sdk-composite-session", &result.content);
649        let validator = TreeSitterValidator::new();
650        let validation = validator.validate(&raw_node);
651        assert!(validation.is_valid, "validation failed: {:?}", validation.error);
652
653        let parser = SttpNodeParser::new();
654        let parse = parser.try_parse_strict(&raw_node, "sdk-composite-session");
655        assert!(parse.success, "strict parse failed: {:?}", parse.error);
656    }
657
658    fn render_sttp_node(session_id: &str, content: &Value) -> String {
659        let content_text = render_sttp_value(content);
660        format!(
661            "⊕⟨ {{ trigger: manual, response_format: temporal_node, origin_session: \"{session_id}\", compression_depth: 1, parent_node: null, prime: {{ attractor_config: {{ stability: 0.80, friction: 0.20, logic: 0.85, autonomy: 0.75 }}, context_summary: \"sdk composite conformance\", relevant_tier: raw, retrieval_budget: 5 }} }} ⟩\n\
662⦿⟨ {{ timestamp: \"2026-05-03T00:00:00Z\", tier: raw, session_id: \"{session_id}\", user_avec: {{ stability: 0.80, friction: 0.20, logic: 0.85, autonomy: 0.75, psi: 2.60 }}, model_avec: {{ stability: 0.82, friction: 0.18, logic: 0.84, autonomy: 0.74, psi: 2.58 }} }} ⟩\n\
663◈⟨ {content_text} ⟩\n\
664⍉⟨ {{ rho: 0.96, kappa: 0.94, psi: 2.60, compression_avec: {{ stability: 0.81, friction: 0.19, logic: 0.84, autonomy: 0.74, psi: 2.58 }} }} ⟩"
665        )
666    }
667
668    fn render_sttp_value(value: &Value) -> String {
669        match value {
670            Value::Null => "null".to_string(),
671            Value::Bool(v) => v.to_string(),
672            Value::Number(v) => v.to_string(),
673            Value::String(v) => format!("\"{}\"", v.replace('"', "\\\"")),
674            Value::Array(values) => {
675                let rendered = values
676                    .iter()
677                    .map(render_sttp_value)
678                    .collect::<Vec<_>>()
679                    .join(", ");
680                format!("[{rendered}]")
681            }
682            Value::Object(obj) => render_sttp_object(obj),
683        }
684    }
685
686    fn render_sttp_object(obj: &Map<String, Value>) -> String {
687        let rendered = obj
688            .iter()
689            .map(|(key, value)| format!("{key}: {}", render_sttp_value(value)))
690            .collect::<Vec<_>>()
691            .join(", ");
692        format!("{{ {rendered} }}")
693    }
694
695    fn test_node(session_id: &str, timestamp: chrono::DateTime<Utc>, raw: &str) -> SttpNode {
696        let state = AvecState {
697            stability: 0.6,
698            friction: 0.4,
699            logic: 0.8,
700            autonomy: 0.7,
701        };
702
703        SttpNode {
704            raw: raw.to_string(),
705            session_id: session_id.to_string(),
706            tier: "raw".to_string(),
707            timestamp,
708            compression_depth: 1,
709            parent_node_id: None,
710            sync_key: format!("{}:{}", session_id, timestamp.timestamp_nanos_opt().unwrap_or_default()),
711            updated_at: timestamp,
712            source_metadata: None,
713            context_summary: Some(raw.to_string()),
714            embedding_dimensions: None,
715            embedding_model: None,
716            embedding: None,
717            embedded_at: None,
718            user_avec: state,
719            model_avec: state,
720            compression_avec: Some(state),
721            rho: 0.9,
722            kappa: 0.8,
723            psi: 2.5,
724        }
725    }
726}