pub mod config;
pub mod handlers;
pub mod rerank;
pub mod scoring;
pub mod tunable;
use std::sync::Mutex;
use async_trait::async_trait;
use serde_json::Value;
use khive_runtime::pack::PackRuntime;
use khive_runtime::{KhiveRuntime, NamespaceToken, RuntimeError, VerbRegistry};
use khive_types::{HandlerDef, Pack, ParamDef, VerbCategory, Visibility};
use crate::config::RecallConfig;
pub struct MemoryPack {
runtime: KhiveRuntime,
config: Mutex<RecallConfig>,
}
impl MemoryPack {
pub(crate) fn active_config(&self) -> RecallConfig {
self.config.lock().unwrap().clone()
}
}
impl Pack for MemoryPack {
const NAME: &'static str = "memory";
const NOTE_KINDS: &'static [&'static str] = &["memory"];
const ENTITY_KINDS: &'static [&'static str] = &[];
const HANDLERS: &'static [HandlerDef] = &MEMORY_HANDLERS;
const REQUIRES: &'static [&'static str] = &["kg"];
}
static MEMORY_HANDLERS: [HandlerDef; 7] = [
HandlerDef {
name: "memory.remember",
description: "Create a memory note with salience and decay",
visibility: Visibility::Verb,
category: VerbCategory::Commissive,
params: &[
ParamDef {
name: "content",
param_type: "string",
required: true,
description: "Memory content to store.",
},
ParamDef {
name: "salience",
param_type: "number",
required: false,
description: "Salience weight 0.0–1.0 (default 0.5).",
},
ParamDef {
name: "decay_factor",
param_type: "number",
required: false,
description: "Decay rate >= 0 (default 0.01). Higher = faster decay. At 0.01 the effective half-life is ~69 days.",
},
ParamDef {
name: "memory_type",
param_type: "string",
required: false,
description: "Memory type tag: \"episodic\" | \"semantic\" (default \"episodic\"). Other values are rejected.",
},
ParamDef {
name: "source_id",
param_type: "string",
required: false,
description: "UUID or 8-char short ID of the entity or note this memory annotates.",
},
ParamDef {
name: "embedding_model",
param_type: "string",
required: false,
description: "Model name for vector embedding (must be registered). Defaults to pack-configured model.",
},
ParamDef {
name: "tags",
param_type: "array",
required: false,
description: "Tag values to filter by. Matched against properties.tags on stored memories.",
},
],
},
HandlerDef {
name: "memory.recall",
description: "Recall memory notes with decay-aware hybrid ranking",
visibility: Visibility::Verb,
category: VerbCategory::Assertive,
params: &[
ParamDef {
name: "query",
param_type: "string",
required: true,
description: "Semantic recall query.",
},
ParamDef {
name: "limit",
param_type: "integer",
required: false,
description: "Maximum memories to return (default 10).",
},
ParamDef {
name: "top_k",
param_type: "integer",
required: false,
description: "Override result limit (max 100). Takes priority over limit.",
},
ParamDef {
name: "min_score",
param_type: "number",
required: false,
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.",
},
ParamDef {
name: "score_floor",
param_type: "number",
required: false,
description: "Alias for min_score. Filter out hits below this composite score. Scores are always in [0,1] regardless of fusion strategy.",
},
ParamDef {
name: "min_salience",
param_type: "number",
required: false,
description: "Minimum salience score filter.",
},
ParamDef {
name: "memory_type",
param_type: "string",
required: false,
description: "Filter to this memory_type.",
},
ParamDef {
name: "fusion_strategy",
param_type: "string",
required: false,
description: "Fusion strategy: \"rrf\" | \"weighted\" | \"union\" | \"vector_only\" | \"keyword_only\". Weighted values come from pack config.",
},
ParamDef {
name: "embedding_model",
param_type: "string",
required: false,
description: "Model name for vector recall (must be registered). Defaults to pack-configured model.",
},
ParamDef {
name: "include_breakdown",
param_type: "boolean",
required: false,
description: "Include per-component score breakdowns in results.",
},
ParamDef {
name: "entity_names",
param_type: "array",
required: false,
description: "Entity names to boost in scoring. Memories mentioning these entities receive a 1.3× score multiplier.",
},
ParamDef {
name: "full_content",
param_type: "boolean",
required: false,
description: "When false, content is truncated to 200 chars in results. Default true.",
},
ParamDef {
name: "tags",
param_type: "array",
required: false,
description: "Filter results to memories whose stored tags include at least one (any) or all (all) of these values. Matched against properties.tags.",
},
ParamDef {
name: "tag_mode",
param_type: "string",
required: false,
description: "Tag filter mode: \"any\" (OR, default) or \"all\" (AND). Only applies when tags is non-empty.",
},
],
},
HandlerDef {
name: "memory.recall_embed",
description: "Return the embedding vector used by memory recall",
visibility: Visibility::Subhandler,
category: VerbCategory::Assertive,
params: &[ParamDef {
name: "include_embeddings",
param_type: "boolean",
required: false,
description: "When true, include full embedding vector arrays in the response. Default false — only model name and dimension metadata are returned.",
}],
},
HandlerDef {
name: "memory.recall_candidates",
description: "Return raw memory recall candidates by retrieval source",
visibility: Visibility::Subhandler,
category: VerbCategory::Assertive,
params: &[],
},
HandlerDef {
name: "memory.recall_fuse",
description: "Return fused memory recall candidates before final scoring",
visibility: Visibility::Subhandler,
category: VerbCategory::Assertive,
params: &[],
},
HandlerDef {
name: "memory.recall_rerank",
description: "Apply configured rerankers to fused candidates (ADR-033 §2)",
visibility: Visibility::Subhandler,
category: VerbCategory::Assertive,
params: &[],
},
HandlerDef {
name: "memory.recall_score",
description: "Score a memory recall candidate and return score breakdown",
visibility: Visibility::Subhandler,
category: VerbCategory::Assertive,
params: &[],
},
];
impl MemoryPack {
pub fn new(runtime: KhiveRuntime) -> Self {
Self {
runtime,
config: Mutex::new(RecallConfig::default()),
}
}
}
struct MemoryPackFactory;
impl khive_runtime::PackFactory for MemoryPackFactory {
fn name(&self) -> &'static str {
"memory"
}
fn requires(&self) -> &'static [&'static str] {
&["kg"]
}
fn create(&self, runtime: KhiveRuntime) -> Box<dyn khive_runtime::PackRuntime> {
Box::new(MemoryPack::new(runtime))
}
}
inventory::submit! { khive_runtime::PackRegistration(&MemoryPackFactory) }
#[async_trait]
impl PackRuntime for MemoryPack {
fn name(&self) -> &str {
<MemoryPack as Pack>::NAME
}
fn note_kinds(&self) -> &'static [&'static str] {
<MemoryPack as Pack>::NOTE_KINDS
}
fn entity_kinds(&self) -> &'static [&'static str] {
<MemoryPack as Pack>::ENTITY_KINDS
}
fn handlers(&self) -> &'static [HandlerDef] {
&MEMORY_HANDLERS
}
fn requires(&self) -> &'static [&'static str] {
<MemoryPack as Pack>::REQUIRES
}
async fn dispatch(
&self,
verb: &str,
params: Value,
registry: &VerbRegistry,
token: &NamespaceToken,
) -> Result<Value, RuntimeError> {
match verb {
"memory.remember" => self.handle_remember(token, params).await,
"memory.recall" => self.handle_recall(token, params, registry).await,
"memory.recall_embed" => self.handle_recall_embed(params).await,
"memory.recall_candidates" => self.handle_recall_candidates(token, params).await,
"memory.recall_fuse" => self.handle_recall_fuse(token, params, registry).await,
"memory.recall_rerank" => self.handle_recall_rerank(params).await,
"memory.recall_score" => self.handle_recall_score(params).await,
_ => Err(RuntimeError::InvalidInput(format!(
"memory pack does not handle verb {verb:?}"
))),
}
}
}