feat: Vestige v1.5.0 — Cognitive Engine, memory dreaming, graph exploration, predictive retrieval

28-module CognitiveEngine with full neuroscience pipeline on every tool call.
FSRS-6 now fully automatic: periodic consolidation (6h timer + inline every
100 tool calls), real retrievability formula, episodic-to-semantic auto-merge,
cross-memory reinforcement, Park et al. triple retrieval scoring, ACT-R
base-level activation, personalized w20 optimization.

New tools (19 → 23):
- dream: memory consolidation via replay, discovers hidden connections
- explore_connections: graph traversal (chain, associations, bridges)
- predict: proactive retrieval based on context and activity patterns
- restore: memory restore from JSON backups

All existing tools upgraded with cognitive pre/post processing pipelines.
33 files changed, ~4,100 lines added.
This commit is contained in:
Sam Valladares 2026-02-18 23:34:15 -06:00
parent 3fce1f0b70
commit 927f41c3e4
34 changed files with 4302 additions and 266 deletions

View file

@ -1,13 +1,21 @@
//! Ingest Tool
//!
//! Add new knowledge to memory.
//!
//! v1.5.0: Enhanced with same cognitive pipeline as smart_ingest:
//! Pre-ingest: importance scoring + intent detection
//! Post-ingest: synaptic tagging + novelty model update + hippocampal indexing
use chrono::Utc;
use serde::Deserialize;
use serde_json::Value;
use std::sync::Arc;
use tokio::sync::Mutex;
use vestige_core::{IngestInput, Storage};
use crate::cognitive::CognitiveEngine;
use vestige_core::{
ContentType, ImportanceContext, ImportanceEvent, ImportanceEventType, IngestInput, Storage,
};
/// Input schema for ingest tool
pub fn schema() -> Value {
@ -48,6 +56,7 @@ struct IngestArgs {
pub async fn execute(
storage: &Arc<Mutex<Storage>>,
cognitive: &Arc<Mutex<CognitiveEngine>>,
args: Option<Value>,
) -> Result<Value, String> {
let args: IngestArgs = match args {
@ -64,45 +73,103 @@ pub async fn execute(
return Err("Content too large (max 1MB)".to_string());
}
// ====================================================================
// COGNITIVE PRE-INGEST: importance scoring + intent detection
// ====================================================================
let mut importance_composite = 0.0_f64;
let mut tags = args.tags.unwrap_or_default();
let mut is_novel = false;
let mut embedding_strategy = String::new();
if let Ok(cog) = cognitive.try_lock() {
// Full 4-channel importance scoring
let context = ImportanceContext::current();
let importance = cog.importance_signals.compute_importance(&args.content, &context);
importance_composite = importance.composite;
// Standalone novelty check (dopaminergic signal)
let novelty_ctx = vestige_core::neuroscience::importance_signals::Context::default();
is_novel = cog.novelty_signal.is_novel(&args.content, &novelty_ctx);
// Intent detection → auto-tag
let intent_result = cog.intent_detector.detect_intent();
if intent_result.confidence > 0.5 {
let intent_tag = format!("intent:{:?}", intent_result.primary_intent);
let intent_tag = if intent_tag.len() > 50 {
format!("{}...", &intent_tag[..47])
} else {
intent_tag
};
tags.push(intent_tag);
}
// Detect content type → select adaptive embedding strategy
let content_type = ContentType::detect(&args.content);
let strategy = cog.adaptive_embedder.select_strategy(&content_type);
embedding_strategy = format!("{:?}", strategy);
}
let input = IngestInput {
content: args.content,
content: args.content.clone(),
node_type: args.node_type.unwrap_or_else(|| "fact".to_string()),
source: args.source,
sentiment_score: 0.0,
sentiment_magnitude: 0.0,
tags: args.tags.unwrap_or_default(),
sentiment_magnitude: importance_composite,
tags,
valid_from: None,
valid_until: None,
};
let mut storage = storage.lock().await;
// ====================================================================
// INGEST (storage lock)
// ====================================================================
let mut storage_guard = storage.lock().await;
// Route through smart_ingest when embeddings are available to prevent duplicates.
// Falls back to raw ingest only when embeddings aren't ready.
#[cfg(all(feature = "embeddings", feature = "vector-search"))]
{
let fallback_input = input.clone();
match storage.smart_ingest(input) {
match storage_guard.smart_ingest(input) {
Ok(result) => {
let node_id = result.node.id.clone();
let node_content = result.node.content.clone();
let node_type = result.node.node_type.clone();
let has_embedding = result.node.has_embedding.unwrap_or(false);
drop(storage_guard);
run_post_ingest(cognitive, &node_id, &node_content, &node_type, importance_composite);
return Ok(serde_json::json!({
"success": true,
"nodeId": result.node.id,
"nodeId": node_id,
"decision": result.decision,
"message": format!("Knowledge ingested successfully. Node ID: {} ({})", result.node.id, result.decision),
"hasEmbedding": result.node.has_embedding.unwrap_or(false),
"message": format!("Knowledge ingested successfully. Node ID: {} ({})", node_id, result.decision),
"hasEmbedding": has_embedding,
"similarity": result.similarity,
"reason": result.reason,
"isNovel": is_novel,
"embeddingStrategy": embedding_strategy,
}));
}
Err(_) => {
// smart_ingest failed — fall through to raw ingest with cloned input
let node = storage.ingest(fallback_input).map_err(|e| e.to_string())?;
let node = storage_guard.ingest(fallback_input).map_err(|e| e.to_string())?;
let node_id = node.id.clone();
let node_content = node.content.clone();
let node_type = node.node_type.clone();
let has_embedding = node.has_embedding.unwrap_or(false);
drop(storage_guard);
run_post_ingest(cognitive, &node_id, &node_content, &node_type, importance_composite);
return Ok(serde_json::json!({
"success": true,
"nodeId": node.id,
"nodeId": node_id,
"decision": "create",
"message": format!("Knowledge ingested successfully. Node ID: {}", node.id),
"hasEmbedding": node.has_embedding.unwrap_or(false),
"message": format!("Knowledge ingested successfully. Node ID: {}", node_id),
"hasEmbedding": has_embedding,
"isNovel": is_novel,
"embeddingStrategy": embedding_strategy,
}));
}
}
@ -111,17 +178,62 @@ pub async fn execute(
// Fallback for builds without embedding features
#[cfg(not(all(feature = "embeddings", feature = "vector-search")))]
{
let node = storage.ingest(input).map_err(|e| e.to_string())?;
let node = storage_guard.ingest(input).map_err(|e| e.to_string())?;
let node_id = node.id.clone();
let node_content = node.content.clone();
let node_type = node.node_type.clone();
let has_embedding = node.has_embedding.unwrap_or(false);
drop(storage_guard);
run_post_ingest(cognitive, &node_id, &node_content, &node_type, importance_composite);
Ok(serde_json::json!({
"success": true,
"nodeId": node.id,
"nodeId": node_id,
"decision": "create",
"message": format!("Knowledge ingested successfully. Node ID: {}", node.id),
"hasEmbedding": node.has_embedding.unwrap_or(false),
"message": format!("Knowledge ingested successfully. Node ID: {}", node_id),
"hasEmbedding": has_embedding,
"isNovel": is_novel,
"embeddingStrategy": embedding_strategy,
}))
}
}
/// Cognitive post-ingest side effects: synaptic tagging, novelty update, hippocampal indexing.
fn run_post_ingest(
cognitive: &Arc<Mutex<CognitiveEngine>>,
node_id: &str,
content: &str,
node_type: &str,
importance_composite: f64,
) {
if let Ok(mut cog) = cognitive.try_lock() {
// Synaptic tagging for retroactive capture
if importance_composite > 0.3 {
cog.synaptic_tagging.tag_memory(node_id);
if importance_composite > 0.7 {
let event = ImportanceEvent::for_memory(node_id, ImportanceEventType::NoveltySpike);
let _capture = cog.synaptic_tagging.trigger_prp(event);
}
}
// Update novelty model
cog.importance_signals.learn_content(content);
// Record in hippocampal index
let _ = cog.hippocampal_index.index_memory(
node_id,
content,
node_type,
Utc::now(),
None,
);
// Cross-project pattern recording
cog.cross_project.record_project_memory(node_id, "default", None);
}
}
// ============================================================================
// TESTS
// ============================================================================
@ -129,8 +241,13 @@ pub async fn execute(
#[cfg(test)]
mod tests {
use super::*;
use crate::cognitive::CognitiveEngine;
use tempfile::TempDir;
fn test_cognitive() -> Arc<Mutex<CognitiveEngine>> {
Arc::new(Mutex::new(CognitiveEngine::new()))
}
/// Create a test storage instance with a temporary database
async fn test_storage() -> (Arc<Mutex<Storage>>, TempDir) {
let dir = TempDir::new().unwrap();
@ -146,7 +263,7 @@ mod tests {
async fn test_ingest_empty_content_fails() {
let (storage, _dir) = test_storage().await;
let args = serde_json::json!({ "content": "" });
let result = execute(&storage, Some(args)).await;
let result = execute(&storage, &test_cognitive(), Some(args)).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("empty"));
}
@ -155,7 +272,7 @@ mod tests {
async fn test_ingest_whitespace_only_content_fails() {
let (storage, _dir) = test_storage().await;
let args = serde_json::json!({ "content": " \n\t " });
let result = execute(&storage, Some(args)).await;
let result = execute(&storage, &test_cognitive(), Some(args)).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("empty"));
}
@ -163,7 +280,7 @@ mod tests {
#[tokio::test]
async fn test_ingest_missing_arguments_fails() {
let (storage, _dir) = test_storage().await;
let result = execute(&storage, None).await;
let result = execute(&storage, &test_cognitive(), None).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Missing arguments"));
}
@ -172,7 +289,7 @@ mod tests {
async fn test_ingest_missing_content_field_fails() {
let (storage, _dir) = test_storage().await;
let args = serde_json::json!({ "node_type": "fact" });
let result = execute(&storage, Some(args)).await;
let result = execute(&storage, &test_cognitive(), Some(args)).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("Invalid arguments"));
}
@ -187,7 +304,7 @@ mod tests {
// Create content larger than 1MB
let large_content = "x".repeat(1_000_001);
let args = serde_json::json!({ "content": large_content });
let result = execute(&storage, Some(args)).await;
let result = execute(&storage, &test_cognitive(), Some(args)).await;
assert!(result.is_err());
assert!(result.unwrap_err().contains("too large"));
}
@ -198,7 +315,7 @@ mod tests {
// Create content exactly 1MB
let exact_content = "x".repeat(1_000_000);
let args = serde_json::json!({ "content": exact_content });
let result = execute(&storage, Some(args)).await;
let result = execute(&storage, &test_cognitive(), Some(args)).await;
assert!(result.is_ok());
}
@ -212,7 +329,7 @@ mod tests {
let args = serde_json::json!({
"content": "This is a test fact to remember."
});
let result = execute(&storage, Some(args)).await;
let result = execute(&storage, &test_cognitive(), Some(args)).await;
assert!(result.is_ok());
let value = result.unwrap();
@ -228,7 +345,7 @@ mod tests {
"content": "Error handling should use Result<T, E> pattern.",
"node_type": "pattern"
});
let result = execute(&storage, Some(args)).await;
let result = execute(&storage, &test_cognitive(), Some(args)).await;
assert!(result.is_ok());
let value = result.unwrap();
@ -242,7 +359,7 @@ mod tests {
"content": "The Rust programming language emphasizes safety.",
"tags": ["rust", "programming", "safety"]
});
let result = execute(&storage, Some(args)).await;
let result = execute(&storage, &test_cognitive(), Some(args)).await;
assert!(result.is_ok());
let value = result.unwrap();
@ -256,7 +373,7 @@ mod tests {
"content": "MCP protocol version 2024-11-05 is the current standard.",
"source": "https://modelcontextprotocol.io/spec"
});
let result = execute(&storage, Some(args)).await;
let result = execute(&storage, &test_cognitive(), Some(args)).await;
assert!(result.is_ok());
let value = result.unwrap();
@ -272,7 +389,7 @@ mod tests {
"tags": ["architecture", "design"],
"source": "team meeting notes"
});
let result = execute(&storage, Some(args)).await;
let result = execute(&storage, &test_cognitive(), Some(args)).await;
assert!(result.is_ok());
let value = result.unwrap();
@ -290,7 +407,7 @@ mod tests {
let args = serde_json::json!({
"content": "Default type test content."
});
let result = execute(&storage, Some(args)).await;
let result = execute(&storage, &test_cognitive(), Some(args)).await;
assert!(result.is_ok());
// Verify node was created - the default type is "fact"