Skip to main content

kmp_memory_api/
memory_views.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5/// One node of a recalled bundle, as a consumer sees it.
6///
7/// A projection, never the aggregate. The kernel's node gains fields as its
8/// domain needs them; a consumer that read it directly would inherit each one
9/// as a contract.
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub struct MemoryNodeView {
12    pub node_id: String,
13    pub node_kind: String,
14    pub title: String,
15    pub summary: String,
16    pub status: String,
17    pub labels: Vec<String>,
18    pub properties: BTreeMap<String, String>,
19}
20
21/// One relationship between recalled nodes.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct MemoryRelationshipView {
24    pub source_node_id: String,
25    pub target_node_id: String,
26    pub relationship_type: String,
27    /// The stated reason for the link, when its recorder gave one. A consumer
28    /// surfacing a conflict quotes this instead of inventing a rationale.
29    pub why: Option<String>,
30    /// The evidence the link cites, when its recorder cited any.
31    pub evidence: Option<String>,
32}
33
34/// Full detail for one node, with the hash that makes it citable.
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub struct MemoryDetailView {
37    pub node_id: String,
38    pub detail: String,
39    pub content_hash: String,
40    pub revision: u64,
41}
42
43/// How well the rendering served its budget.
44///
45/// The kernel's own account of the trade it made: how much raw memory the
46/// rendering stands in for, and what was kept against what was let go. Ratios
47/// are `f64`, which is why this view — and everything holding it — is
48/// `PartialEq` and not `Eq`.
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
50pub struct MemoryQualityView {
51    /// Tokens the un-rendered memory would have cost.
52    pub raw_equivalent_tokens: u32,
53    pub compression_ratio: f64,
54    pub causal_density: f64,
55    pub noise_ratio: f64,
56    pub detail_coverage: f64,
57}
58
59/// The rendered context, ready for a reader.
60///
61/// `content_hash` covers `content` exactly: a consumer that hands the text to
62/// a model can verify the model received what the kernel rendered, and cite
63/// the hash instead of quoting itself.
64#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
65pub struct RenderedMemoryView {
66    pub content: String,
67    pub content_hash: String,
68    pub token_count: u32,
69    pub quality: MemoryQualityView,
70}
71
72/// What one recall returned.
73#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
74pub struct MemoryRecallView {
75    /// The about that was asked for, echoed back unchanged.
76    pub about: String,
77    /// The revision of the memory this recall was answered from. Two recalls
78    /// answering with the same `revision` and `content_hash` saw one
79    /// snapshot; a consumer combining recalls checks this instead of hoping.
80    pub revision: u64,
81    /// Hash of the memory state behind this recall — the snapshot's identity,
82    /// distinct from `rendered.content_hash`, which covers the rendered text.
83    pub content_hash: String,
84    pub root: MemoryNodeView,
85    pub neighbors: Vec<MemoryNodeView>,
86    pub relationships: Vec<MemoryRelationshipView>,
87    pub details: Vec<MemoryDetailView>,
88    pub rendered: RenderedMemoryView,
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    fn node(id: &str) -> MemoryNodeView {
96        MemoryNodeView {
97            node_id: id.to_string(),
98            node_kind: "decision".to_string(),
99            title: "First decision".to_string(),
100            summary: "What was decided".to_string(),
101            status: "active".to_string(),
102            labels: vec!["timeline".to_string()],
103            properties: BTreeMap::new(),
104        }
105    }
106
107    #[test]
108    fn a_recall_survives_the_wire() {
109        let view = MemoryRecallView {
110            about: "project:checkout".to_string(),
111            revision: 7,
112            content_hash: "snapshot-hash".to_string(),
113            root: node("about:project:checkout"),
114            neighbors: vec![node("decision:first")],
115            relationships: vec![MemoryRelationshipView {
116                source_node_id: "about:project:checkout".to_string(),
117                target_node_id: "decision:first".to_string(),
118                relationship_type: "contains".to_string(),
119                why: Some("the about holds its decisions".to_string()),
120                evidence: None,
121            }],
122            details: vec![MemoryDetailView {
123                node_id: "decision:first".to_string(),
124                detail: "The full text.".to_string(),
125                content_hash: "abc".to_string(),
126                revision: 1,
127            }],
128            rendered: RenderedMemoryView {
129                content: "# Context".to_string(),
130                content_hash: "def".to_string(),
131                token_count: 3,
132                quality: MemoryQualityView {
133                    raw_equivalent_tokens: 12,
134                    compression_ratio: 4.0,
135                    causal_density: 0.5,
136                    noise_ratio: 0.1,
137                    detail_coverage: 0.9,
138                },
139            },
140        };
141        let bytes = serde_json::to_vec(&view).expect("serializes");
142        assert_eq!(
143            serde_json::from_slice::<MemoryRecallView>(&bytes).expect("deserializes"),
144            view
145        );
146    }
147}