Skip to main content

khive_pack_memory/
lib.rs

1pub mod config;
2pub mod handlers;
3pub mod rerank;
4pub mod scoring;
5pub mod tunable;
6
7use std::sync::Mutex;
8
9use async_trait::async_trait;
10use serde_json::Value;
11
12use khive_runtime::pack::PackRuntime;
13use khive_runtime::{KhiveRuntime, NamespaceToken, RuntimeError, VerbRegistry};
14use khive_types::{HandlerDef, Pack, ParamDef, VerbCategory, Visibility};
15
16use crate::config::RecallConfig;
17
18pub struct MemoryPack {
19    runtime: KhiveRuntime,
20    /// Active recall config.
21    config: Mutex<RecallConfig>,
22}
23
24impl MemoryPack {
25    /// Return a clone of the current active `RecallConfig`.
26    ///
27    /// Handlers call this to pick up the latest tuned parameters.
28    pub(crate) fn active_config(&self) -> RecallConfig {
29        self.config.lock().unwrap().clone()
30    }
31}
32
33impl Pack for MemoryPack {
34    const NAME: &'static str = "memory";
35    const NOTE_KINDS: &'static [&'static str] = &["memory"];
36    const ENTITY_KINDS: &'static [&'static str] = &[];
37    const HANDLERS: &'static [HandlerDef] = &MEMORY_HANDLERS;
38    const REQUIRES: &'static [&'static str] = &["kg"];
39}
40
41// ADR-025: Illocutionary classification (Searle 1976)
42//   Commissive — commits caller to a persistent change
43//   Assertive  — retrieves/presents state of affairs
44static MEMORY_HANDLERS: [HandlerDef; 7] = [
45    // Commissive: commits a memory to the namespace
46    HandlerDef {
47        name: "memory.remember",
48        description: "Create a memory note with salience and decay",
49        visibility: Visibility::Verb,
50        category: VerbCategory::Commissive,
51        params: &[
52            ParamDef {
53                name: "content",
54                param_type: "string",
55                required: true,
56                description: "Memory content to store.",
57            },
58            ParamDef {
59                name: "salience",
60                param_type: "number",
61                required: false,
62                description: "Salience weight 0.0–1.0 (default 0.5).",
63            },
64            ParamDef {
65                name: "decay_factor",
66                param_type: "number",
67                required: false,
68                description: "Decay rate >= 0 (default 0.01). Higher = faster decay. At 0.01 the effective half-life is ~69 days.",
69            },
70            ParamDef {
71                name: "memory_type",
72                param_type: "string",
73                required: false,
74                description: "Memory type tag: \"episodic\" | \"semantic\" (default \"episodic\"). Other values are rejected.",
75            },
76            ParamDef {
77                name: "source_id",
78                param_type: "string",
79                required: false,
80                description: "UUID or 8-char short ID of the entity or note this memory annotates.",
81            },
82            ParamDef {
83                name: "embedding_model",
84                param_type: "string",
85                required: false,
86                description: "Model name for vector embedding (must be registered). Defaults to pack-configured model.",
87            },
88            ParamDef {
89                name: "tags",
90                param_type: "array",
91                required: false,
92                description: "Tag values to filter by. Matched against properties.tags on stored memories.",
93            },
94        ],
95    },
96    // Assertive: retrieves memory notes via decay-aware ranking
97    HandlerDef {
98        name: "memory.recall",
99        description: "Recall memory notes with decay-aware hybrid ranking",
100        visibility: Visibility::Verb,
101        category: VerbCategory::Assertive,
102        params: &[
103            ParamDef {
104                name: "query",
105                param_type: "string",
106                required: true,
107                description: "Semantic recall query.",
108            },
109            ParamDef {
110                name: "limit",
111                param_type: "integer",
112                required: false,
113                description: "Maximum memories to return (default 10).",
114            },
115            ParamDef {
116                name: "top_k",
117                param_type: "integer",
118                required: false,
119                description: "Override result limit (max 100). Takes priority over limit.",
120            },
121            ParamDef {
122                name: "min_score",
123                param_type: "number",
124                required: false,
125                description: "Minimum composite score to include (default 0.0). Composite scores are always in [0,1]: relevance is normalized to [0,1] per strategy (RRF rank-1 → 1.0; Weighted scores are [0,1] natively), and all three weighted contributions sum to at most 1.0. Typical production floor: 0.3–0.7.",
126            },
127            ParamDef {
128                name: "score_floor",
129                param_type: "number",
130                required: false,
131                description: "Alias for min_score. Filter out hits below this composite score. Scores are always in [0,1] regardless of fusion strategy.",
132            },
133            ParamDef {
134                name: "min_salience",
135                param_type: "number",
136                required: false,
137                description: "Minimum salience score filter.",
138            },
139            ParamDef {
140                name: "memory_type",
141                param_type: "string",
142                required: false,
143                description: "Filter to this memory_type.",
144            },
145            ParamDef {
146                name: "fusion_strategy",
147                param_type: "string",
148                required: false,
149                description: "Fusion strategy: \"rrf\" | \"weighted\" | \"union\" | \"vector_only\" | \"keyword_only\". Weighted values come from pack config.",
150            },
151            ParamDef {
152                name: "embedding_model",
153                param_type: "string",
154                required: false,
155                description: "Model name for vector recall (must be registered). Defaults to pack-configured model.",
156            },
157            ParamDef {
158                name: "include_breakdown",
159                param_type: "boolean",
160                required: false,
161                description: "Include per-component score breakdowns in results.",
162            },
163            ParamDef {
164                name: "entity_names",
165                param_type: "array",
166                required: false,
167                description: "Entity names to boost in scoring. Memories mentioning these entities receive a 1.3× score multiplier.",
168            },
169            ParamDef {
170                name: "full_content",
171                param_type: "boolean",
172                required: false,
173                description: "When false, content is truncated to 200 chars in results. Default true.",
174            },
175            ParamDef {
176                name: "tags",
177                param_type: "array",
178                required: false,
179                description: "Filter results to memories whose stored tags include at least one (any) or all (all) of these values. Matched against properties.tags.",
180            },
181            ParamDef {
182                name: "tag_mode",
183                param_type: "string",
184                required: false,
185                description: "Tag filter mode: \"any\" (OR, default) or \"all\" (AND). Only applies when tags is non-empty.",
186            },
187        ],
188    },
189    HandlerDef {
190        name: "memory.recall_embed",
191        description: "Return the embedding vector used by memory recall",
192        visibility: Visibility::Subhandler,
193        category: VerbCategory::Assertive,
194        params: &[ParamDef {
195            name: "include_embeddings",
196            param_type: "boolean",
197            required: false,
198            description: "When true, include full embedding vector arrays in the response. Default false — only model name and dimension metadata are returned.",
199        }],
200    },
201    HandlerDef {
202        name: "memory.recall_candidates",
203        description: "Return raw memory recall candidates by retrieval source",
204        visibility: Visibility::Subhandler,
205        category: VerbCategory::Assertive,
206        params: &[],
207    },
208    HandlerDef {
209        name: "memory.recall_fuse",
210        description: "Return fused memory recall candidates before final scoring",
211        visibility: Visibility::Subhandler,
212        category: VerbCategory::Assertive,
213        params: &[],
214    },
215    // ADR-033 §2, F222: rerank stage between fuse and score
216    HandlerDef {
217        name: "memory.recall_rerank",
218        description: "Apply configured rerankers to fused candidates (ADR-033 §2)",
219        visibility: Visibility::Subhandler,
220        category: VerbCategory::Assertive,
221        params: &[],
222    },
223    HandlerDef {
224        name: "memory.recall_score",
225        description: "Score a memory recall candidate and return score breakdown",
226        visibility: Visibility::Subhandler,
227        category: VerbCategory::Assertive,
228        params: &[],
229    },
230];
231
232impl MemoryPack {
233    pub fn new(runtime: KhiveRuntime) -> Self {
234        Self {
235            runtime,
236            config: Mutex::new(RecallConfig::default()),
237        }
238    }
239}
240
241// ── ADR-027: inventory self-registration ─────────────────────────────────────
242
243struct MemoryPackFactory;
244
245impl khive_runtime::PackFactory for MemoryPackFactory {
246    fn name(&self) -> &'static str {
247        "memory"
248    }
249
250    fn requires(&self) -> &'static [&'static str] {
251        &["kg"]
252    }
253
254    fn create(&self, runtime: KhiveRuntime) -> Box<dyn khive_runtime::PackRuntime> {
255        Box::new(MemoryPack::new(runtime))
256    }
257}
258
259inventory::submit! { khive_runtime::PackRegistration(&MemoryPackFactory) }
260
261#[async_trait]
262impl PackRuntime for MemoryPack {
263    fn name(&self) -> &str {
264        <MemoryPack as Pack>::NAME
265    }
266
267    fn note_kinds(&self) -> &'static [&'static str] {
268        <MemoryPack as Pack>::NOTE_KINDS
269    }
270
271    fn entity_kinds(&self) -> &'static [&'static str] {
272        <MemoryPack as Pack>::ENTITY_KINDS
273    }
274
275    fn handlers(&self) -> &'static [HandlerDef] {
276        &MEMORY_HANDLERS
277    }
278
279    fn requires(&self) -> &'static [&'static str] {
280        <MemoryPack as Pack>::REQUIRES
281    }
282
283    async fn dispatch(
284        &self,
285        verb: &str,
286        params: Value,
287        registry: &VerbRegistry,
288        token: &NamespaceToken,
289    ) -> Result<Value, RuntimeError> {
290        match verb {
291            "memory.remember" => self.handle_remember(token, params).await,
292            "memory.recall" => self.handle_recall(token, params, registry).await,
293            "memory.recall_embed" => self.handle_recall_embed(params).await,
294            "memory.recall_candidates" => self.handle_recall_candidates(token, params).await,
295            "memory.recall_fuse" => self.handle_recall_fuse(token, params, registry).await,
296            "memory.recall_rerank" => self.handle_recall_rerank(params).await,
297            "memory.recall_score" => self.handle_recall_score(params).await,
298            _ => Err(RuntimeError::InvalidInput(format!(
299                "memory pack does not handle verb {verb:?}"
300            ))),
301        }
302    }
303}