vestige/crates/vestige-mcp/src/tools/context.rs
Sam Valladares 5b90a73055 feat: Vestige v1.9.1 AUTONOMIC — self-regulating memory with graph visualization
Retention Target System: auto-GC low-retention memories during consolidation
(VESTIGE_RETENTION_TARGET env var, default 0.8). Auto-Promote: memories
accessed 3+ times in 24h get frequency-dependent potentiation. Waking SWR
Tagging: promoted memories get preferential 70/30 dream replay. Improved
Consolidation Scheduler: triggers on 6h staleness or 2h active use.

New tools: memory_health (retention dashboard with distribution buckets,
trend tracking, recommendations) and memory_graph (subgraph export with
Fruchterman-Reingold force-directed layout, up to 200 nodes).

Dream connections now persist to database via save_connection(), enabling
memory_graph traversal. Schema Migration V8 adds waking_tag, utility_score,
times_retrieved/useful columns and retention_snapshots table. 21 MCP tools.

v1.9.1 fixes: ConnectionRecord export, UTF-8 safe truncation, link_type
normalization, utility_score clamping, only-new-connections persistence,
70/30 split capacity fill, nonexistent center_id error handling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 02:02:06 -06:00

172 lines
5.7 KiB
Rust

//! Context-Dependent Memory Tool (Deprecated)
//!
//! Retrieval based on encoding context match.
//! Based on Tulving & Thomson's Encoding Specificity Principle (1973).
use chrono::Utc;
use serde_json::Value;
use std::sync::Arc;
use vestige_core::{RecallInput, SearchMode, Storage};
/// Input schema for match_context tool
pub fn schema() -> Value {
serde_json::json!({
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query for content matching"
},
"topics": {
"type": "array",
"items": { "type": "string" },
"description": "Active topics in current context"
},
"project": {
"type": "string",
"description": "Current project name"
},
"mood": {
"type": "string",
"enum": ["positive", "negative", "neutral"],
"description": "Current emotional state"
},
"time_weight": {
"type": "number",
"description": "Weight for temporal context (0.0-1.0, default: 0.3)"
},
"topic_weight": {
"type": "number",
"description": "Weight for topical context (0.0-1.0, default: 0.4)"
},
"limit": {
"type": "integer",
"description": "Maximum results (default: 10)"
}
},
"required": ["query"]
})
}
pub async fn execute(
storage: &Arc<Storage>,
args: Option<Value>,
) -> Result<Value, String> {
let args = args.ok_or("Missing arguments")?;
let query = args["query"]
.as_str()
.ok_or("query is required")?;
let topics: Vec<String> = args["topics"]
.as_array()
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
.unwrap_or_default();
let project = args["project"].as_str().map(String::from);
let mood = args["mood"].as_str().unwrap_or("neutral");
let time_weight = args["time_weight"].as_f64().unwrap_or(0.3);
let topic_weight = args["topic_weight"].as_f64().unwrap_or(0.4);
let limit = args["limit"].as_i64().unwrap_or(10) as i32;
let now = Utc::now();
// Get candidate memories
let recall_input = RecallInput {
query: query.to_string(),
limit: limit * 2, // Get more, then filter
min_retention: 0.0,
search_mode: SearchMode::Hybrid,
valid_at: None,
};
let candidates = storage.recall(recall_input)
.map_err(|e| e.to_string())?;
// Score by context match (simplified implementation)
let mut scored_results: Vec<_> = candidates.into_iter()
.map(|mem| {
// Calculate context score based on:
// 1. Temporal proximity (how recent)
let hours_ago = (now - mem.created_at).num_hours() as f64;
let temporal_score = 1.0 / (1.0 + hours_ago / 24.0); // Decay over days
// 2. Tag overlap with topics
let tag_overlap = if topics.is_empty() {
0.5 // Neutral if no topics specified
} else {
let matching = mem.tags.iter()
.filter(|t| topics.iter().any(|topic| topic.to_lowercase().contains(&t.to_lowercase())))
.count();
matching as f64 / topics.len().max(1) as f64
};
// 3. Project match
let project_score = match (&project, &mem.source) {
(Some(p), Some(s)) if s.to_lowercase().contains(&p.to_lowercase()) => 1.0,
(Some(_), None) => 0.0,
(None, _) => 0.5,
_ => 0.3,
};
// 4. Emotional match (simplified)
let mood_score = match mood {
"positive" if mem.sentiment_score > 0.0 => 0.8,
"negative" if mem.sentiment_score < 0.0 => 0.8,
"neutral" if mem.sentiment_score.abs() < 0.3 => 0.8,
_ => 0.5,
};
// Combine scores
let context_score = temporal_score * time_weight
+ tag_overlap * topic_weight
+ project_score * 0.2
+ mood_score * 0.1;
let combined_score = mem.retention_strength * 0.5 + context_score * 0.5;
(mem, context_score, combined_score)
})
.collect();
// Sort by combined score (handle NaN safely)
scored_results.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal));
scored_results.truncate(limit as usize);
let results: Vec<Value> = scored_results.into_iter()
.map(|(mem, ctx_score, combined)| {
serde_json::json!({
"id": mem.id,
"content": mem.content,
"retentionStrength": mem.retention_strength,
"contextScore": ctx_score,
"combinedScore": combined,
"tags": mem.tags,
"createdAt": mem.created_at.to_rfc3339()
})
})
.collect();
Ok(serde_json::json!({
"success": true,
"query": query,
"currentContext": {
"topics": topics,
"project": project,
"mood": mood
},
"weights": {
"temporal": time_weight,
"topical": topic_weight
},
"resultCount": results.len(),
"results": results,
"science": {
"theory": "Encoding Specificity Principle (Tulving & Thomson, 1973)",
"principle": "Memory retrieval is most effective when retrieval context matches encoding context"
}
}))
}