mirror of
https://github.com/samvallad33/vestige.git
synced 2026-04-25 00:36:22 +02:00
feat: Vestige v2.0.0 "Cognitive Leap" — 3D dashboard, HyDE search, WebSocket events
The biggest release in Vestige history. Complete visual and cognitive overhaul. Dashboard: - SvelteKit 2 + Three.js 3D neural visualization at localhost:3927/dashboard - 7 interactive pages: Graph, Memories, Timeline, Feed, Explore, Intentions, Stats - WebSocket event bus with 16 event types, real-time 3D animations - Bloom post-processing, GPU instanced rendering, force-directed layout - Dream visualization mode, FSRS retention curves, command palette (Cmd+K) - Keyboard shortcuts, responsive mobile layout, PWA installable - Single binary deployment via include_dir! (22MB) Engine: - HyDE query expansion (intent classification + 3-5 semantic variants + centroid) - fastembed 5.11 with optional Nomic v2 MoE + Qwen3 reranker + Metal GPU - Emotional memory module (#29) - Criterion benchmark suite Backend: - Axum WebSocket at /ws with heartbeat + event broadcast - 7 new REST endpoints for cognitive operations - Event emission from MCP tools via shared broadcast channel - CORS for SvelteKit dev mode Distribution: - GitHub issue templates (bug report, feature request) - CHANGELOG with comprehensive v2.0 release notes - README updated with dashboard docs, architecture diagram, comparison table 734 tests passing, zero warnings, 22MB release binary. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
26cee040a5
commit
c2d28f3433
321 changed files with 32695 additions and 4727 deletions
|
|
@ -6,10 +6,12 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use chrono::Utc;
|
||||
use tokio::sync::{broadcast, Mutex};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::cognitive::CognitiveEngine;
|
||||
use vestige_mcp::dashboard::events::VestigeEvent;
|
||||
use crate::protocol::messages::{
|
||||
CallToolRequest, CallToolResult, InitializeRequest, InitializeResult,
|
||||
ListResourcesResult, ListToolsResult, ReadResourceRequest, ReadResourceResult,
|
||||
|
|
@ -27,15 +29,41 @@ pub struct McpServer {
|
|||
initialized: bool,
|
||||
/// Tool call counter for inline consolidation trigger (every 100 calls)
|
||||
tool_call_count: AtomicU64,
|
||||
/// Optional event broadcast channel for dashboard real-time updates.
|
||||
event_tx: Option<broadcast::Sender<VestigeEvent>>,
|
||||
}
|
||||
|
||||
impl McpServer {
|
||||
#[allow(dead_code)]
|
||||
pub fn new(storage: Arc<Storage>, cognitive: Arc<Mutex<CognitiveEngine>>) -> Self {
|
||||
Self {
|
||||
storage,
|
||||
cognitive,
|
||||
initialized: false,
|
||||
tool_call_count: AtomicU64::new(0),
|
||||
event_tx: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an MCP server that broadcasts events to the dashboard.
|
||||
pub fn new_with_events(
|
||||
storage: Arc<Storage>,
|
||||
cognitive: Arc<Mutex<CognitiveEngine>>,
|
||||
event_tx: broadcast::Sender<VestigeEvent>,
|
||||
) -> Self {
|
||||
Self {
|
||||
storage,
|
||||
cognitive,
|
||||
initialized: false,
|
||||
tool_call_count: AtomicU64::new(0),
|
||||
event_tx: Some(event_tx),
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit an event to the dashboard (no-op if no event channel).
|
||||
fn emit(&self, event: VestigeEvent) {
|
||||
if let Some(ref tx) = self.event_tx {
|
||||
let _ = tx.send(event);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -143,7 +171,7 @@ impl McpServer {
|
|||
},
|
||||
ToolDescription {
|
||||
name: "memory".to_string(),
|
||||
description: Some("Unified memory management tool. Actions: 'get' (retrieve full node), 'delete' (remove memory), 'state' (get accessibility state), 'promote' (thumbs up — increases retrieval strength), 'demote' (thumbs down — decreases retrieval strength, does NOT delete).".to_string()),
|
||||
description: Some("Unified memory management tool. Actions: 'get' (retrieve full node), 'delete' (remove memory), 'state' (get accessibility state), 'promote' (thumbs up — increases retrieval strength), 'demote' (thumbs down — decreases retrieval strength, does NOT delete), 'edit' (update content in-place, preserves FSRS state).".to_string()),
|
||||
input_schema: tools::memory_unified::schema(),
|
||||
},
|
||||
ToolDescription {
|
||||
|
|
@ -287,6 +315,9 @@ impl McpServer {
|
|||
cog.consolidation_scheduler.record_activity();
|
||||
}
|
||||
|
||||
// Save args for event emission (tool dispatch consumes request.arguments)
|
||||
let saved_args = if self.event_tx.is_some() { request.arguments.clone() } else { None };
|
||||
|
||||
let result = match request.name.as_str() {
|
||||
// ================================================================
|
||||
// UNIFIED TOOLS (v1.1+) - Preferred API
|
||||
|
|
@ -611,6 +642,14 @@ impl McpServer {
|
|||
}
|
||||
};
|
||||
|
||||
// ================================================================
|
||||
// DASHBOARD EVENT EMISSION (v2.0)
|
||||
// Emit real-time events to WebSocket clients after successful tool calls.
|
||||
// ================================================================
|
||||
if let Ok(ref content) = result {
|
||||
self.emit_tool_event(&request.name, &saved_args, content);
|
||||
}
|
||||
|
||||
let response = match result {
|
||||
Ok(content) => {
|
||||
let call_result = CallToolResult {
|
||||
|
|
@ -784,6 +823,196 @@ impl McpServer {
|
|||
Err(e) => Err(JsonRpcError::internal_error(&e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract event data from tool results and emit to dashboard.
|
||||
fn emit_tool_event(
|
||||
&self,
|
||||
tool_name: &str,
|
||||
args: &Option<serde_json::Value>,
|
||||
result: &serde_json::Value,
|
||||
) {
|
||||
if self.event_tx.is_none() {
|
||||
return;
|
||||
}
|
||||
let now = Utc::now();
|
||||
|
||||
match tool_name {
|
||||
// -- smart_ingest: memory created/updated --
|
||||
"smart_ingest" | "ingest" | "session_checkpoint" => {
|
||||
// Single mode: result has "action" (created/updated/superseded/reinforced)
|
||||
if let Some(action) = result.get("action").and_then(|a| a.as_str()) {
|
||||
let id = result.get("nodeId").or(result.get("id"))
|
||||
.and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let preview = result.get("contentPreview").or(result.get("content"))
|
||||
.and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
match action {
|
||||
"created" => {
|
||||
let node_type = result.get("nodeType")
|
||||
.and_then(|v| v.as_str()).unwrap_or("fact").to_string();
|
||||
let tags = result.get("tags")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| arr.iter().filter_map(|t| t.as_str().map(String::from)).collect())
|
||||
.unwrap_or_default();
|
||||
self.emit(VestigeEvent::MemoryCreated {
|
||||
id, content_preview: preview, node_type, tags, timestamp: now,
|
||||
});
|
||||
}
|
||||
"updated" | "superseded" | "reinforced" => {
|
||||
self.emit(VestigeEvent::MemoryUpdated {
|
||||
id, content_preview: preview, field: action.to_string(), timestamp: now,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// Batch mode: result has "results" array
|
||||
if let Some(results) = result.get("results").and_then(|r| r.as_array()) {
|
||||
for item in results {
|
||||
let action = item.get("action").and_then(|a| a.as_str()).unwrap_or("");
|
||||
let id = item.get("nodeId").or(item.get("id"))
|
||||
.and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let preview = item.get("contentPreview")
|
||||
.and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
if action == "created" {
|
||||
self.emit(VestigeEvent::MemoryCreated {
|
||||
id, content_preview: preview,
|
||||
node_type: "fact".to_string(), tags: vec![], timestamp: now,
|
||||
});
|
||||
} else if !action.is_empty() {
|
||||
self.emit(VestigeEvent::MemoryUpdated {
|
||||
id, content_preview: preview,
|
||||
field: action.to_string(), timestamp: now,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- memory: get/delete/promote/demote --
|
||||
"memory" | "promote_memory" | "demote_memory" | "delete_knowledge" | "get_memory_state" => {
|
||||
let action = args.as_ref()
|
||||
.and_then(|a| a.get("action"))
|
||||
.and_then(|a| a.as_str())
|
||||
.unwrap_or(if tool_name == "promote_memory" { "promote" }
|
||||
else if tool_name == "demote_memory" { "demote" }
|
||||
else if tool_name == "delete_knowledge" { "delete" }
|
||||
else { "" });
|
||||
let id = args.as_ref()
|
||||
.and_then(|a| a.get("id"))
|
||||
.and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
match action {
|
||||
"delete" => {
|
||||
self.emit(VestigeEvent::MemoryDeleted { id, timestamp: now });
|
||||
}
|
||||
"promote" => {
|
||||
let retention = result.get("newRetention")
|
||||
.or(result.get("retrievalStrength"))
|
||||
.and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
self.emit(VestigeEvent::MemoryPromoted {
|
||||
id, new_retention: retention, timestamp: now,
|
||||
});
|
||||
}
|
||||
"demote" => {
|
||||
let retention = result.get("newRetention")
|
||||
.or(result.get("retrievalStrength"))
|
||||
.and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
self.emit(VestigeEvent::MemoryDemoted {
|
||||
id, new_retention: retention, timestamp: now,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// -- search --
|
||||
"search" | "recall" | "semantic_search" | "hybrid_search" => {
|
||||
let query = args.as_ref()
|
||||
.and_then(|a| a.get("query"))
|
||||
.and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let results = result.get("results").and_then(|r| r.as_array());
|
||||
let result_count = results.map(|r| r.len()).unwrap_or(0);
|
||||
let result_ids: Vec<String> = results
|
||||
.map(|r| r.iter()
|
||||
.filter_map(|item| item.get("id").and_then(|v| v.as_str()).map(String::from))
|
||||
.collect())
|
||||
.unwrap_or_default();
|
||||
let duration_ms = result.get("durationMs")
|
||||
.or(result.get("duration_ms"))
|
||||
.and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
self.emit(VestigeEvent::SearchPerformed {
|
||||
query, result_count, result_ids, duration_ms, timestamp: now,
|
||||
});
|
||||
}
|
||||
|
||||
// -- dream --
|
||||
"dream" => {
|
||||
let replayed = result.get("memoriesReplayed")
|
||||
.or(result.get("memories_replayed"))
|
||||
.and_then(|v| v.as_u64()).unwrap_or(0) as usize;
|
||||
let connections = result.get("connectionsFound")
|
||||
.or(result.get("connections_found"))
|
||||
.and_then(|v| v.as_u64()).unwrap_or(0) as usize;
|
||||
let insights = result.get("insightsGenerated")
|
||||
.or(result.get("insights"))
|
||||
.and_then(|v| v.as_array()).map(|a| a.len()).unwrap_or(0);
|
||||
let duration_ms = result.get("durationMs")
|
||||
.or(result.get("duration_ms"))
|
||||
.and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
self.emit(VestigeEvent::DreamCompleted {
|
||||
memories_replayed: replayed, connections_found: connections,
|
||||
insights_generated: insights, duration_ms, timestamp: now,
|
||||
});
|
||||
}
|
||||
|
||||
// -- consolidate --
|
||||
"consolidate" => {
|
||||
let processed = result.get("nodesProcessed")
|
||||
.or(result.get("nodes_processed"))
|
||||
.and_then(|v| v.as_u64()).unwrap_or(0) as usize;
|
||||
let decay = result.get("decayApplied")
|
||||
.or(result.get("decay_applied"))
|
||||
.and_then(|v| v.as_u64()).unwrap_or(0) as usize;
|
||||
let embeddings = result.get("embeddingsGenerated")
|
||||
.or(result.get("embeddings_generated"))
|
||||
.and_then(|v| v.as_u64()).unwrap_or(0) as usize;
|
||||
let duration_ms = result.get("durationMs")
|
||||
.or(result.get("duration_ms"))
|
||||
.and_then(|v| v.as_u64()).unwrap_or(0);
|
||||
self.emit(VestigeEvent::ConsolidationCompleted {
|
||||
nodes_processed: processed, decay_applied: decay,
|
||||
embeddings_generated: embeddings, duration_ms, timestamp: now,
|
||||
});
|
||||
}
|
||||
|
||||
// -- importance_score --
|
||||
"importance_score" => {
|
||||
let preview = args.as_ref()
|
||||
.and_then(|a| a.get("content"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| if s.len() > 100 { format!("{}...", &s[..100]) } else { s.to_string() })
|
||||
.unwrap_or_default();
|
||||
let composite = result.get("compositeScore")
|
||||
.or(result.get("composite_score"))
|
||||
.and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
let channels = result.get("channels").or(result.get("breakdown"));
|
||||
let novelty = channels.and_then(|c| c.get("novelty"))
|
||||
.and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
let arousal = channels.and_then(|c| c.get("arousal"))
|
||||
.and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
let reward = channels.and_then(|c| c.get("reward"))
|
||||
.and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
let attention = channels.and_then(|c| c.get("attention"))
|
||||
.and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||
self.emit(VestigeEvent::ImportanceScored {
|
||||
content_preview: preview, composite_score: composite,
|
||||
novelty, arousal, reward, attention, timestamp: now,
|
||||
});
|
||||
}
|
||||
|
||||
// Other tools don't emit events
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue