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 config: Mutex<RecallConfig>,
22}
23
24impl MemoryPack {
25 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
41static MEMORY_HANDLERS: [HandlerDef; 7] = [
45 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 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 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
241struct 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}