mirror of
https://github.com/samvallad33/vestige.git
synced 2026-04-24 16:26:22 +02:00
chore: remove 3,091 LOC of orphan code + fix ghost env-var docs
Some checks are pending
CI / Test (macos-latest) (push) Waiting to run
CI / Test (ubuntu-latest) (push) Waiting to run
CI / Release Build (aarch64-apple-darwin) (push) Blocked by required conditions
CI / Release Build (x86_64-unknown-linux-gnu) (push) Blocked by required conditions
CI / Release Build (x86_64-apple-darwin) (push) Blocked by required conditions
Test Suite / Unit Tests (push) Waiting to run
Test Suite / MCP E2E Tests (push) Waiting to run
Test Suite / User Journey Tests (push) Blocked by required conditions
Test Suite / Dashboard Build (push) Waiting to run
Test Suite / Code Coverage (push) Waiting to run
Some checks are pending
CI / Test (macos-latest) (push) Waiting to run
CI / Test (ubuntu-latest) (push) Waiting to run
CI / Release Build (aarch64-apple-darwin) (push) Blocked by required conditions
CI / Release Build (x86_64-unknown-linux-gnu) (push) Blocked by required conditions
CI / Release Build (x86_64-apple-darwin) (push) Blocked by required conditions
Test Suite / Unit Tests (push) Waiting to run
Test Suite / MCP E2E Tests (push) Waiting to run
Test Suite / User Journey Tests (push) Blocked by required conditions
Test Suite / Dashboard Build (push) Waiting to run
Test Suite / Code Coverage (push) Waiting to run
Nine tool modules in crates/vestige-mcp/src/tools/ had zero callers after
the v2.0.x unification work shipped *_unified + maintenance::* replacements.
They'd been #[allow(dead_code)]-papered over and forgotten. Verified each
module independently: grep for tools::<name>::, string dispatch in server.rs,
cross-crate usage — all nine returned zero external callers.
Removed modules (all superseded):
checkpoint (364 LOC) — no callers anywhere
codebase (298) — superseded by codebase_unified
consolidate (36) — superseded by maintenance::execute_consolidate
ingest (456) — superseded by smart_ingest
intentions (1,093) — superseded by intention_unified
knowledge (106) — no callers anywhere
recall (403) — superseded by search_unified
search (184) — superseded by search_unified
stats (132) — superseded by maintenance::execute_system_status
Also removed:
- EmotionCategory::base_arousal (10 LOC, zero callers)
Kept (still string-dispatched from server.rs):
- context, feedback, memory_states, review, tagging
Doc fixes (ghost env vars that were documented but zero Rust source reads):
- docs/CONFIGURATION.md — dropped VESTIGE_DATA_DIR, VESTIGE_LOG_LEVEL rows
(neither is read anywhere; --data-dir CLI flag + RUST_LOG are the real
mechanisms). Added the full real env-var table.
- packages/vestige-mcp-npm/README.md — same two ghost rows dropped
- docs/VESTIGE_STATE_AND_PLAN.md:399 — dropped VESTIGE_DATA_DIR row
- docs/VESTIGE_STATE_AND_PLAN.md:709 — typo VESTIGE_API_KEY
-> VESTIGE_AUTH_TOKEN (matches shipping convention), "open if unset"
-> "auto-generated if unset" to match actual behavior
Verified post-cleanup:
- cargo check --workspace clean
- cargo clippy --workspace -D warnings clean
- cargo test --workspace 1,223 passing / 0 failed
- cargo build --release -p vestige-mcp clean
Net: -3,091 LOC (14 files), zero behavior change, zero regressions.
This commit is contained in:
parent
6a807698ef
commit
0e9b260518
14 changed files with 26 additions and 3117 deletions
|
|
@ -103,21 +103,6 @@ pub enum EmotionCategory {
|
|||
Neutral,
|
||||
}
|
||||
|
||||
impl EmotionCategory {
|
||||
/// Get the base arousal level for this category
|
||||
#[allow(dead_code)]
|
||||
fn base_arousal(&self) -> f64 {
|
||||
match self {
|
||||
Self::Joy => 0.6,
|
||||
Self::Frustration => 0.7,
|
||||
Self::Urgency => 0.9,
|
||||
Self::Surprise => 0.8,
|
||||
Self::Confusion => 0.4,
|
||||
Self::Neutral => 0.1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for EmotionCategory {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
|
|
|
|||
|
|
@ -1,364 +0,0 @@
|
|||
//! Session Checkpoint Tool
|
||||
//!
|
||||
//! Batch smart_ingest for session-end saves. Accepts up to 20 items
|
||||
//! in a single call, routing each through Prediction Error Gating.
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
|
||||
use vestige_core::{IngestInput, Storage};
|
||||
|
||||
/// Input schema for session_checkpoint tool
|
||||
pub fn schema() -> Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"description": "Array of items to save (max 20). Each goes through Prediction Error Gating.",
|
||||
"maxItems": 20,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "The content to remember"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Tags for categorization"
|
||||
},
|
||||
"node_type": {
|
||||
"type": "string",
|
||||
"description": "Type: fact, concept, event, person, place, note, pattern, decision",
|
||||
"default": "fact"
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"description": "Source reference"
|
||||
}
|
||||
},
|
||||
"required": ["content"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["items"]
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CheckpointArgs {
|
||||
items: Vec<CheckpointItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct CheckpointItem {
|
||||
content: String,
|
||||
tags: Option<Vec<String>>,
|
||||
node_type: Option<String>,
|
||||
source: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn execute(storage: &Arc<Storage>, args: Option<Value>) -> Result<Value, String> {
|
||||
let args: CheckpointArgs = match args {
|
||||
Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?,
|
||||
None => return Err("Missing arguments".to_string()),
|
||||
};
|
||||
|
||||
if args.items.is_empty() {
|
||||
return Err("Items array cannot be empty".to_string());
|
||||
}
|
||||
|
||||
if args.items.len() > 20 {
|
||||
return Err("Maximum 20 items per checkpoint".to_string());
|
||||
}
|
||||
|
||||
let mut results = Vec::new();
|
||||
let mut created = 0u32;
|
||||
let mut updated = 0u32;
|
||||
let mut skipped = 0u32;
|
||||
let mut errors = 0u32;
|
||||
|
||||
for (i, item) in args.items.into_iter().enumerate() {
|
||||
if item.content.trim().is_empty() {
|
||||
results.push(serde_json::json!({
|
||||
"index": i,
|
||||
"status": "skipped",
|
||||
"reason": "Empty content"
|
||||
}));
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let input = IngestInput {
|
||||
content: item.content,
|
||||
node_type: item.node_type.unwrap_or_else(|| "fact".to_string()),
|
||||
source: item.source,
|
||||
sentiment_score: 0.0,
|
||||
sentiment_magnitude: 0.0,
|
||||
tags: item.tags.unwrap_or_default(),
|
||||
valid_from: None,
|
||||
valid_until: None,
|
||||
};
|
||||
|
||||
#[cfg(all(feature = "embeddings", feature = "vector-search"))]
|
||||
{
|
||||
match storage.smart_ingest(input) {
|
||||
Ok(result) => {
|
||||
match result.decision.as_str() {
|
||||
"create" | "supersede" | "replace" => created += 1,
|
||||
"update" | "reinforce" | "merge" | "add_context" => updated += 1,
|
||||
_ => created += 1,
|
||||
}
|
||||
results.push(serde_json::json!({
|
||||
"index": i,
|
||||
"status": "saved",
|
||||
"decision": result.decision,
|
||||
"nodeId": result.node.id,
|
||||
"similarity": result.similarity,
|
||||
"reason": result.reason
|
||||
}));
|
||||
}
|
||||
Err(e) => {
|
||||
errors += 1;
|
||||
results.push(serde_json::json!({
|
||||
"index": i,
|
||||
"status": "error",
|
||||
"reason": e.to_string()
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(all(feature = "embeddings", feature = "vector-search")))]
|
||||
{
|
||||
match storage.ingest(input) {
|
||||
Ok(node) => {
|
||||
created += 1;
|
||||
results.push(serde_json::json!({
|
||||
"index": i,
|
||||
"status": "saved",
|
||||
"decision": "create",
|
||||
"nodeId": node.id,
|
||||
"reason": "Embeddings not available - used regular ingest"
|
||||
}));
|
||||
}
|
||||
Err(e) => {
|
||||
errors += 1;
|
||||
results.push(serde_json::json!({
|
||||
"index": i,
|
||||
"status": "error",
|
||||
"reason": e.to_string()
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"success": errors == 0,
|
||||
"summary": {
|
||||
"total": results.len(),
|
||||
"created": created,
|
||||
"updated": updated,
|
||||
"skipped": skipped,
|
||||
"errors": errors
|
||||
},
|
||||
"results": results
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
async fn test_storage() -> (Arc<Storage>, TempDir) {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let storage = Storage::new(Some(dir.path().join("test.db"))).unwrap();
|
||||
(Arc::new(storage), dir)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_schema_has_required_fields() {
|
||||
let schema = schema();
|
||||
assert_eq!(schema["type"], "object");
|
||||
assert!(schema["properties"]["items"].is_object());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_empty_items_fails() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
let result = execute(&storage, Some(serde_json::json!({ "items": [] }))).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_batch_ingest() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
let result = execute(
|
||||
&storage,
|
||||
Some(serde_json::json!({
|
||||
"items": [
|
||||
{ "content": "First checkpoint item", "tags": ["test"] },
|
||||
{ "content": "Second checkpoint item", "tags": ["test"] }
|
||||
]
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
let value = result.unwrap();
|
||||
assert_eq!(value["summary"]["total"], 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_skips_empty_content() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
let result = execute(
|
||||
&storage,
|
||||
Some(serde_json::json!({
|
||||
"items": [
|
||||
{ "content": "Valid item" },
|
||||
{ "content": "" },
|
||||
{ "content": "Another valid item" }
|
||||
]
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
let value = result.unwrap();
|
||||
assert_eq!(value["summary"]["skipped"], 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_missing_args_fails() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
let result = execute(&storage, None).await;
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("Missing arguments"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_exceeds_20_items_fails() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
let items: Vec<serde_json::Value> = (0..21)
|
||||
.map(|i| serde_json::json!({ "content": format!("Item {}", i) }))
|
||||
.collect();
|
||||
let result = execute(&storage, Some(serde_json::json!({ "items": items }))).await;
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("Maximum 20 items"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_exactly_20_items_succeeds() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
let items: Vec<serde_json::Value> = (0..20)
|
||||
.map(|i| serde_json::json!({ "content": format!("Item {}", i) }))
|
||||
.collect();
|
||||
let result = execute(&storage, Some(serde_json::json!({ "items": items }))).await;
|
||||
assert!(result.is_ok());
|
||||
let value = result.unwrap();
|
||||
assert_eq!(value["summary"]["total"], 20);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_skips_whitespace_only_content() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
let result = execute(
|
||||
&storage,
|
||||
Some(serde_json::json!({
|
||||
"items": [
|
||||
{ "content": " \t\n " },
|
||||
{ "content": "Valid content" }
|
||||
]
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
let value = result.unwrap();
|
||||
assert_eq!(value["summary"]["skipped"], 1);
|
||||
assert_eq!(value["summary"]["created"], 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_single_item_succeeds() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
let result = execute(
|
||||
&storage,
|
||||
Some(serde_json::json!({
|
||||
"items": [{ "content": "Single item" }]
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
let value = result.unwrap();
|
||||
assert_eq!(value["summary"]["total"], 1);
|
||||
assert_eq!(value["success"], true);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_items_with_all_fields() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
let result = execute(
|
||||
&storage,
|
||||
Some(serde_json::json!({
|
||||
"items": [{
|
||||
"content": "Full fields item",
|
||||
"tags": ["test", "checkpoint"],
|
||||
"node_type": "decision",
|
||||
"source": "test-suite"
|
||||
}]
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
assert!(result.is_ok());
|
||||
let value = result.unwrap();
|
||||
assert_eq!(value["summary"]["created"], 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_results_array_matches_items() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
let result = execute(
|
||||
&storage,
|
||||
Some(serde_json::json!({
|
||||
"items": [
|
||||
{ "content": "First" },
|
||||
{ "content": "" },
|
||||
{ "content": "Third" }
|
||||
]
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
let value = result.unwrap();
|
||||
let results = value["results"].as_array().unwrap();
|
||||
assert_eq!(results.len(), 3);
|
||||
assert_eq!(results[0]["index"], 0);
|
||||
assert_eq!(results[1]["index"], 1);
|
||||
assert_eq!(results[1]["status"], "skipped");
|
||||
assert_eq!(results[2]["index"], 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_success_false_when_errors() {
|
||||
// All items empty = all skipped = 0 errors = success true
|
||||
let (storage, _dir) = test_storage().await;
|
||||
let result = execute(
|
||||
&storage,
|
||||
Some(serde_json::json!({
|
||||
"items": [
|
||||
{ "content": "" },
|
||||
{ "content": " " }
|
||||
]
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
let value = result.unwrap();
|
||||
assert_eq!(value["success"], true); // skipped ≠ errors
|
||||
assert_eq!(value["summary"]["errors"], 0);
|
||||
assert_eq!(value["summary"]["skipped"], 2);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,298 +0,0 @@
|
|||
//! Codebase Tools (Deprecated - use codebase_unified instead)
|
||||
//!
|
||||
//! Remember patterns, decisions, and context about codebases.
|
||||
//! This is a differentiating feature for AI-assisted development.
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
|
||||
use vestige_core::{IngestInput, Storage};
|
||||
|
||||
/// Input schema for remember_pattern tool
|
||||
pub fn pattern_schema() -> Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name/title for this pattern"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Detailed description of the pattern"
|
||||
},
|
||||
"files": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Files where this pattern is used"
|
||||
},
|
||||
"codebase": {
|
||||
"type": "string",
|
||||
"description": "Codebase/project identifier (e.g., 'vestige-tauri')"
|
||||
}
|
||||
},
|
||||
"required": ["name", "description"]
|
||||
})
|
||||
}
|
||||
|
||||
/// Input schema for remember_decision tool
|
||||
pub fn decision_schema() -> Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"decision": {
|
||||
"type": "string",
|
||||
"description": "The architectural or design decision made"
|
||||
},
|
||||
"rationale": {
|
||||
"type": "string",
|
||||
"description": "Why this decision was made"
|
||||
},
|
||||
"alternatives": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Alternatives that were considered"
|
||||
},
|
||||
"files": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Files affected by this decision"
|
||||
},
|
||||
"codebase": {
|
||||
"type": "string",
|
||||
"description": "Codebase/project identifier"
|
||||
}
|
||||
},
|
||||
"required": ["decision", "rationale"]
|
||||
})
|
||||
}
|
||||
|
||||
/// Input schema for get_codebase_context tool
|
||||
pub fn context_schema() -> Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"codebase": {
|
||||
"type": "string",
|
||||
"description": "Codebase/project identifier to get context for"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum items per category (default: 10)",
|
||||
"default": 10
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct PatternArgs {
|
||||
name: String,
|
||||
description: String,
|
||||
files: Option<Vec<String>>,
|
||||
codebase: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DecisionArgs {
|
||||
decision: String,
|
||||
rationale: String,
|
||||
alternatives: Option<Vec<String>>,
|
||||
files: Option<Vec<String>>,
|
||||
codebase: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ContextArgs {
|
||||
codebase: Option<String>,
|
||||
limit: Option<i32>,
|
||||
}
|
||||
|
||||
pub async fn execute_pattern(storage: &Arc<Storage>, args: Option<Value>) -> Result<Value, String> {
|
||||
let args: PatternArgs = match args {
|
||||
Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?,
|
||||
None => return Err("Missing arguments".to_string()),
|
||||
};
|
||||
|
||||
if args.name.trim().is_empty() {
|
||||
return Err("Pattern name cannot be empty".to_string());
|
||||
}
|
||||
|
||||
// Build content with structured format
|
||||
let mut content = format!("# Code Pattern: {}\n\n{}", args.name, args.description);
|
||||
|
||||
if let Some(ref files) = args.files
|
||||
&& !files.is_empty()
|
||||
{
|
||||
content.push_str("\n\n## Files:\n");
|
||||
for f in files {
|
||||
content.push_str(&format!("- {}\n", f));
|
||||
}
|
||||
}
|
||||
|
||||
// Build tags
|
||||
let mut tags = vec!["pattern".to_string(), "codebase".to_string()];
|
||||
if let Some(ref codebase) = args.codebase {
|
||||
tags.push(format!("codebase:{}", codebase));
|
||||
}
|
||||
|
||||
let input = IngestInput {
|
||||
content,
|
||||
node_type: "pattern".to_string(),
|
||||
source: args.codebase.clone(),
|
||||
sentiment_score: 0.0,
|
||||
sentiment_magnitude: 0.0,
|
||||
tags,
|
||||
valid_from: None,
|
||||
valid_until: None,
|
||||
};
|
||||
|
||||
let node = storage.ingest(input).map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"success": true,
|
||||
"nodeId": node.id,
|
||||
"patternName": args.name,
|
||||
"message": format!("Pattern '{}' remembered successfully", args.name),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn execute_decision(
|
||||
storage: &Arc<Storage>,
|
||||
args: Option<Value>,
|
||||
) -> Result<Value, String> {
|
||||
let args: DecisionArgs = match args {
|
||||
Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?,
|
||||
None => return Err("Missing arguments".to_string()),
|
||||
};
|
||||
|
||||
if args.decision.trim().is_empty() {
|
||||
return Err("Decision cannot be empty".to_string());
|
||||
}
|
||||
|
||||
// Build content with structured format (ADR-like)
|
||||
let mut content = format!(
|
||||
"# Decision: {}\n\n## Context\n\n{}\n\n## Decision\n\n{}",
|
||||
&args.decision[..args.decision.len().min(50)],
|
||||
args.rationale,
|
||||
args.decision
|
||||
);
|
||||
|
||||
if let Some(ref alternatives) = args.alternatives
|
||||
&& !alternatives.is_empty()
|
||||
{
|
||||
content.push_str("\n\n## Alternatives Considered:\n");
|
||||
for alt in alternatives {
|
||||
content.push_str(&format!("- {}\n", alt));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref files) = args.files
|
||||
&& !files.is_empty()
|
||||
{
|
||||
content.push_str("\n\n## Affected Files:\n");
|
||||
for f in files {
|
||||
content.push_str(&format!("- {}\n", f));
|
||||
}
|
||||
}
|
||||
|
||||
// Build tags
|
||||
let mut tags = vec![
|
||||
"decision".to_string(),
|
||||
"architecture".to_string(),
|
||||
"codebase".to_string(),
|
||||
];
|
||||
if let Some(ref codebase) = args.codebase {
|
||||
tags.push(format!("codebase:{}", codebase));
|
||||
}
|
||||
|
||||
let input = IngestInput {
|
||||
content,
|
||||
node_type: "decision".to_string(),
|
||||
source: args.codebase.clone(),
|
||||
sentiment_score: 0.0,
|
||||
sentiment_magnitude: 0.0,
|
||||
tags,
|
||||
valid_from: None,
|
||||
valid_until: None,
|
||||
};
|
||||
|
||||
let node = storage.ingest(input).map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"success": true,
|
||||
"nodeId": node.id,
|
||||
"message": "Architectural decision remembered successfully",
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn execute_context(storage: &Arc<Storage>, args: Option<Value>) -> Result<Value, String> {
|
||||
let args: ContextArgs = args
|
||||
.map(serde_json::from_value)
|
||||
.transpose()
|
||||
.map_err(|e| format!("Invalid arguments: {}", e))?
|
||||
.unwrap_or(ContextArgs {
|
||||
codebase: None,
|
||||
limit: Some(10),
|
||||
});
|
||||
|
||||
let limit = args.limit.unwrap_or(10).clamp(1, 50);
|
||||
|
||||
// Build tag filter for codebase
|
||||
// Tags are stored as: ["pattern", "codebase", "codebase:vestige"]
|
||||
// We search for the "codebase:{name}" tag
|
||||
let tag_filter = args.codebase.as_ref().map(|cb| format!("codebase:{}", cb));
|
||||
|
||||
// Query patterns by node_type and tag
|
||||
let patterns = storage
|
||||
.get_nodes_by_type_and_tag("pattern", tag_filter.as_deref(), limit)
|
||||
.unwrap_or_default();
|
||||
|
||||
// Query decisions by node_type and tag
|
||||
let decisions = storage
|
||||
.get_nodes_by_type_and_tag("decision", tag_filter.as_deref(), limit)
|
||||
.unwrap_or_default();
|
||||
|
||||
let formatted_patterns: Vec<Value> = patterns
|
||||
.iter()
|
||||
.map(|n| {
|
||||
serde_json::json!({
|
||||
"id": n.id,
|
||||
"content": n.content,
|
||||
"tags": n.tags,
|
||||
"retentionStrength": n.retention_strength,
|
||||
"createdAt": n.created_at.to_rfc3339(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let formatted_decisions: Vec<Value> = decisions
|
||||
.iter()
|
||||
.map(|n| {
|
||||
serde_json::json!({
|
||||
"id": n.id,
|
||||
"content": n.content,
|
||||
"tags": n.tags,
|
||||
"retentionStrength": n.retention_strength,
|
||||
"createdAt": n.created_at.to_rfc3339(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"codebase": args.codebase,
|
||||
"patterns": {
|
||||
"count": formatted_patterns.len(),
|
||||
"items": formatted_patterns,
|
||||
},
|
||||
"decisions": {
|
||||
"count": formatted_decisions.len(),
|
||||
"items": formatted_decisions,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
//! Consolidation Tool (Deprecated)
|
||||
//!
|
||||
//! Run memory consolidation cycle with FSRS decay and embedding generation.
|
||||
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
|
||||
use vestige_core::Storage;
|
||||
|
||||
/// Input schema for run_consolidation tool
|
||||
pub fn schema() -> Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn execute(storage: &Arc<Storage>) -> Result<Value, String> {
|
||||
let result = storage.run_consolidation().map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"success": true,
|
||||
"nodesProcessed": result.nodes_processed,
|
||||
"nodesPromoted": result.nodes_promoted,
|
||||
"nodesPruned": result.nodes_pruned,
|
||||
"decayApplied": result.decay_applied,
|
||||
"embeddingsGenerated": result.embeddings_generated,
|
||||
"durationMs": result.duration_ms,
|
||||
"message": format!(
|
||||
"Consolidation complete: {} nodes processed, {} embeddings generated, {}ms",
|
||||
result.nodes_processed,
|
||||
result.embeddings_generated,
|
||||
result.duration_ms
|
||||
),
|
||||
}))
|
||||
}
|
||||
|
|
@ -1,456 +0,0 @@
|
|||
//! 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 crate::cognitive::CognitiveEngine;
|
||||
use vestige_core::{
|
||||
ContentType, ImportanceContext, ImportanceEvent, ImportanceEventType, IngestInput, Storage,
|
||||
};
|
||||
|
||||
/// Input schema for ingest tool
|
||||
pub fn schema() -> Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "The content to remember"
|
||||
},
|
||||
"node_type": {
|
||||
"type": "string",
|
||||
"description": "Type of knowledge: fact, concept, event, person, place, note, pattern, decision",
|
||||
"default": "fact"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Tags for categorization"
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"description": "Source or reference for this knowledge"
|
||||
}
|
||||
},
|
||||
"required": ["content"]
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct IngestArgs {
|
||||
content: String,
|
||||
node_type: Option<String>,
|
||||
tags: Option<Vec<String>>,
|
||||
source: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn execute(
|
||||
storage: &Arc<Storage>,
|
||||
cognitive: &Arc<Mutex<CognitiveEngine>>,
|
||||
args: Option<Value>,
|
||||
) -> Result<Value, String> {
|
||||
let args: IngestArgs = match args {
|
||||
Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?,
|
||||
None => return Err("Missing arguments".to_string()),
|
||||
};
|
||||
|
||||
// Validate content
|
||||
if args.content.trim().is_empty() {
|
||||
return Err("Content cannot be empty".to_string());
|
||||
}
|
||||
|
||||
if args.content.len() > 1_000_000 {
|
||||
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[..intent_tag.floor_char_boundary(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.clone(),
|
||||
node_type: args.node_type.unwrap_or_else(|| "fact".to_string()),
|
||||
source: args.source,
|
||||
sentiment_score: 0.0,
|
||||
sentiment_magnitude: importance_composite,
|
||||
tags,
|
||||
valid_from: None,
|
||||
valid_until: None,
|
||||
};
|
||||
|
||||
// ====================================================================
|
||||
// INGEST (storage lock)
|
||||
// ====================================================================
|
||||
|
||||
// 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) {
|
||||
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);
|
||||
|
||||
run_post_ingest(
|
||||
cognitive,
|
||||
&node_id,
|
||||
&node_content,
|
||||
&node_type,
|
||||
importance_composite,
|
||||
);
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"success": true,
|
||||
"nodeId": node_id,
|
||||
"decision": result.decision,
|
||||
"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(_) => {
|
||||
let node = storage.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);
|
||||
|
||||
run_post_ingest(
|
||||
cognitive,
|
||||
&node_id,
|
||||
&node_content,
|
||||
&node_type,
|
||||
importance_composite,
|
||||
);
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"success": true,
|
||||
"nodeId": node_id,
|
||||
"decision": "create",
|
||||
"message": format!("Knowledge ingested successfully. Node ID: {}", node_id),
|
||||
"hasEmbedding": has_embedding,
|
||||
"isNovel": is_novel,
|
||||
"embeddingStrategy": embedding_strategy,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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_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);
|
||||
|
||||
run_post_ingest(
|
||||
cognitive,
|
||||
&node_id,
|
||||
&node_content,
|
||||
&node_type,
|
||||
importance_composite,
|
||||
);
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"success": true,
|
||||
"nodeId": node_id,
|
||||
"decision": "create",
|
||||
"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
|
||||
// ============================================================================
|
||||
|
||||
#[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<Storage>, TempDir) {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let storage = Storage::new(Some(dir.path().join("test.db"))).unwrap();
|
||||
(Arc::new(storage), dir)
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// INPUT VALIDATION TESTS
|
||||
// ========================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ingest_empty_content_fails() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
let args = serde_json::json!({ "content": "" });
|
||||
let result = execute(&storage, &test_cognitive(), Some(args)).await;
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("empty"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
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, &test_cognitive(), Some(args)).await;
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("empty"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ingest_missing_arguments_fails() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
let result = execute(&storage, &test_cognitive(), None).await;
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("Missing arguments"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
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, &test_cognitive(), Some(args)).await;
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("Invalid arguments"));
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// LARGE CONTENT TESTS
|
||||
// ========================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ingest_large_content_fails() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
// 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, &test_cognitive(), Some(args)).await;
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("too large"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ingest_exactly_1mb_succeeds() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
// Create content exactly 1MB
|
||||
let exact_content = "x".repeat(1_000_000);
|
||||
let args = serde_json::json!({ "content": exact_content });
|
||||
let result = execute(&storage, &test_cognitive(), Some(args)).await;
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// SUCCESSFUL INGEST TESTS
|
||||
// ========================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ingest_basic_content_succeeds() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
let args = serde_json::json!({
|
||||
"content": "This is a test fact to remember."
|
||||
});
|
||||
let result = execute(&storage, &test_cognitive(), Some(args)).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let value = result.unwrap();
|
||||
assert_eq!(value["success"], true);
|
||||
assert!(value["nodeId"].is_string());
|
||||
assert!(value["message"].as_str().unwrap().contains("successfully"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ingest_with_node_type() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
let args = serde_json::json!({
|
||||
"content": "Error handling should use Result<T, E> pattern.",
|
||||
"node_type": "pattern"
|
||||
});
|
||||
let result = execute(&storage, &test_cognitive(), Some(args)).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let value = result.unwrap();
|
||||
assert_eq!(value["success"], true);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ingest_with_tags() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
let args = serde_json::json!({
|
||||
"content": "The Rust programming language emphasizes safety.",
|
||||
"tags": ["rust", "programming", "safety"]
|
||||
});
|
||||
let result = execute(&storage, &test_cognitive(), Some(args)).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let value = result.unwrap();
|
||||
assert_eq!(value["success"], true);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ingest_with_source() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
let args = serde_json::json!({
|
||||
"content": "MCP protocol version 2024-11-05 is the current standard.",
|
||||
"source": "https://modelcontextprotocol.io/spec"
|
||||
});
|
||||
let result = execute(&storage, &test_cognitive(), Some(args)).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let value = result.unwrap();
|
||||
assert_eq!(value["success"], true);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ingest_with_all_optional_fields() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
let args = serde_json::json!({
|
||||
"content": "Complex memory with all metadata.",
|
||||
"node_type": "decision",
|
||||
"tags": ["architecture", "design"],
|
||||
"source": "team meeting notes"
|
||||
});
|
||||
let result = execute(&storage, &test_cognitive(), Some(args)).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let value = result.unwrap();
|
||||
assert_eq!(value["success"], true);
|
||||
assert!(value["nodeId"].is_string());
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// NODE TYPE DEFAULTS
|
||||
// ========================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ingest_default_node_type_is_fact() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
let args = serde_json::json!({
|
||||
"content": "Default type test content."
|
||||
});
|
||||
let result = execute(&storage, &test_cognitive(), Some(args)).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
// Verify node was created - the default type is "fact"
|
||||
let node_id = result.unwrap()["nodeId"].as_str().unwrap().to_string();
|
||||
let node = storage.get_node(&node_id).unwrap().unwrap();
|
||||
assert_eq!(node.node_type, "fact");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// SCHEMA TESTS
|
||||
// ========================================================================
|
||||
|
||||
#[test]
|
||||
fn test_schema_has_required_fields() {
|
||||
let schema_value = schema();
|
||||
assert_eq!(schema_value["type"], "object");
|
||||
assert!(schema_value["properties"]["content"].is_object());
|
||||
assert!(
|
||||
schema_value["required"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.contains(&serde_json::json!("content"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_schema_has_optional_fields() {
|
||||
let schema_value = schema();
|
||||
assert!(schema_value["properties"]["node_type"].is_object());
|
||||
assert!(schema_value["properties"]["tags"].is_object());
|
||||
assert!(schema_value["properties"]["source"].is_object());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,106 +0,0 @@
|
|||
//! Knowledge Tools (Deprecated - use memory_unified instead)
|
||||
//!
|
||||
//! Get and delete specific knowledge nodes.
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
|
||||
use vestige_core::Storage;
|
||||
|
||||
/// Input schema for get_knowledge tool
|
||||
pub fn get_schema() -> Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The ID of the knowledge node to retrieve"
|
||||
}
|
||||
},
|
||||
"required": ["id"]
|
||||
})
|
||||
}
|
||||
|
||||
/// Input schema for delete_knowledge tool
|
||||
pub fn delete_schema() -> Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The ID of the knowledge node to delete"
|
||||
}
|
||||
},
|
||||
"required": ["id"]
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct KnowledgeArgs {
|
||||
id: String,
|
||||
}
|
||||
|
||||
pub async fn execute_get(storage: &Arc<Storage>, args: Option<Value>) -> Result<Value, String> {
|
||||
let args: KnowledgeArgs = match args {
|
||||
Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?,
|
||||
None => return Err("Missing arguments".to_string()),
|
||||
};
|
||||
|
||||
// Validate UUID
|
||||
uuid::Uuid::parse_str(&args.id).map_err(|_| "Invalid node ID format".to_string())?;
|
||||
|
||||
let node = storage.get_node(&args.id).map_err(|e| e.to_string())?;
|
||||
|
||||
match node {
|
||||
Some(n) => Ok(serde_json::json!({
|
||||
"found": true,
|
||||
"node": {
|
||||
"id": n.id,
|
||||
"content": n.content,
|
||||
"nodeType": n.node_type,
|
||||
"createdAt": n.created_at.to_rfc3339(),
|
||||
"updatedAt": n.updated_at.to_rfc3339(),
|
||||
"lastAccessed": n.last_accessed.to_rfc3339(),
|
||||
"stability": n.stability,
|
||||
"difficulty": n.difficulty,
|
||||
"reps": n.reps,
|
||||
"lapses": n.lapses,
|
||||
"storageStrength": n.storage_strength,
|
||||
"retrievalStrength": n.retrieval_strength,
|
||||
"retentionStrength": n.retention_strength,
|
||||
"sentimentScore": n.sentiment_score,
|
||||
"sentimentMagnitude": n.sentiment_magnitude,
|
||||
"nextReview": n.next_review.map(|d| d.to_rfc3339()),
|
||||
"source": n.source,
|
||||
"tags": n.tags,
|
||||
"hasEmbedding": n.has_embedding,
|
||||
"embeddingModel": n.embedding_model,
|
||||
}
|
||||
})),
|
||||
None => Ok(serde_json::json!({
|
||||
"found": false,
|
||||
"nodeId": args.id,
|
||||
"message": "Node not found",
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute_delete(storage: &Arc<Storage>, args: Option<Value>) -> Result<Value, String> {
|
||||
let args: KnowledgeArgs = match args {
|
||||
Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?,
|
||||
None => return Err("Missing arguments".to_string()),
|
||||
};
|
||||
|
||||
// Validate UUID
|
||||
uuid::Uuid::parse_str(&args.id).map_err(|_| "Invalid node ID format".to_string())?;
|
||||
|
||||
let deleted = storage.delete_node(&args.id).map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"success": deleted,
|
||||
"nodeId": args.id,
|
||||
"message": if deleted { "Node deleted successfully" } else { "Node not found" },
|
||||
}))
|
||||
}
|
||||
|
|
@ -43,35 +43,23 @@ pub mod cross_reference;
|
|||
// v2.0.5: Active Forgetting — Anderson 2025 + Davis Rac1
|
||||
pub mod suppress;
|
||||
|
||||
// Deprecated/internal tools — not advertised in the public MCP tools/list,
|
||||
// but some functions are actively dispatched for backwards compatibility
|
||||
// and internal cognitive operations. #[allow(dead_code)] suppresses warnings
|
||||
// for the unused schema/struct items within these modules.
|
||||
#[allow(dead_code)]
|
||||
pub mod checkpoint;
|
||||
#[allow(dead_code)]
|
||||
pub mod codebase;
|
||||
#[allow(dead_code)]
|
||||
pub mod consolidate;
|
||||
// Internal/backwards-compat tools still dispatched by server.rs for specific
|
||||
// tool names. Each module below has live callers via string dispatch in
|
||||
// `server.rs` (match arms on request.name). The #[allow(dead_code)]
|
||||
// suppresses warnings for the per-module schema/struct items that aren't
|
||||
// yet consumed.
|
||||
//
|
||||
// The nine legacy siblings here pre-v2.0.8 (checkpoint, codebase, consolidate,
|
||||
// ingest, intentions, knowledge, recall, search, stats) were removed in the
|
||||
// post-v2.0.8 dead-code sweep — all nine had zero callers after the
|
||||
// unification work landed `*_unified` + `maintenance::*` replacements.
|
||||
#[allow(dead_code)]
|
||||
pub mod context;
|
||||
#[allow(dead_code)]
|
||||
pub mod feedback;
|
||||
#[allow(dead_code)]
|
||||
pub mod ingest;
|
||||
#[allow(dead_code)]
|
||||
pub mod intentions;
|
||||
#[allow(dead_code)]
|
||||
pub mod knowledge;
|
||||
#[allow(dead_code)]
|
||||
pub mod memory_states;
|
||||
#[allow(dead_code)]
|
||||
pub mod recall;
|
||||
#[allow(dead_code)]
|
||||
pub mod review;
|
||||
#[allow(dead_code)]
|
||||
pub mod search;
|
||||
#[allow(dead_code)]
|
||||
pub mod stats;
|
||||
#[allow(dead_code)]
|
||||
pub mod tagging;
|
||||
|
|
|
|||
|
|
@ -1,403 +0,0 @@
|
|||
//! Recall Tool (Deprecated - use search_unified instead)
|
||||
//!
|
||||
//! Search and retrieve knowledge from memory.
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
|
||||
use vestige_core::{RecallInput, SearchMode, Storage};
|
||||
|
||||
/// Input schema for recall tool
|
||||
pub fn schema() -> Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Search query"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of results (default: 10)",
|
||||
"default": 10,
|
||||
"minimum": 1,
|
||||
"maximum": 100
|
||||
},
|
||||
"min_retention": {
|
||||
"type": "number",
|
||||
"description": "Minimum retention strength (0.0-1.0, default: 0.0)",
|
||||
"default": 0.0,
|
||||
"minimum": 0.0,
|
||||
"maximum": 1.0
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RecallArgs {
|
||||
query: String,
|
||||
limit: Option<i32>,
|
||||
min_retention: Option<f64>,
|
||||
}
|
||||
|
||||
pub async fn execute(storage: &Arc<Storage>, args: Option<Value>) -> Result<Value, String> {
|
||||
let args: RecallArgs = match args {
|
||||
Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?,
|
||||
None => return Err("Missing arguments".to_string()),
|
||||
};
|
||||
|
||||
if args.query.trim().is_empty() {
|
||||
return Err("Query cannot be empty".to_string());
|
||||
}
|
||||
|
||||
let input = RecallInput {
|
||||
query: args.query.clone(),
|
||||
limit: args.limit.unwrap_or(10).clamp(1, 100),
|
||||
min_retention: args.min_retention.unwrap_or(0.0).clamp(0.0, 1.0),
|
||||
search_mode: SearchMode::Hybrid,
|
||||
valid_at: None,
|
||||
};
|
||||
|
||||
let nodes = storage.recall(input).map_err(|e| e.to_string())?;
|
||||
|
||||
let results: Vec<Value> = nodes
|
||||
.iter()
|
||||
.map(|n| {
|
||||
serde_json::json!({
|
||||
"id": n.id,
|
||||
"content": n.content,
|
||||
"nodeType": n.node_type,
|
||||
"retentionStrength": n.retention_strength,
|
||||
"stability": n.stability,
|
||||
"difficulty": n.difficulty,
|
||||
"reps": n.reps,
|
||||
"tags": n.tags,
|
||||
"source": n.source,
|
||||
"createdAt": n.created_at.to_rfc3339(),
|
||||
"lastAccessed": n.last_accessed.to_rfc3339(),
|
||||
"nextReview": n.next_review.map(|d| d.to_rfc3339()),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"query": args.query,
|
||||
"total": results.len(),
|
||||
"results": results,
|
||||
}))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TESTS
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
use vestige_core::IngestInput;
|
||||
|
||||
/// Create a test storage instance with a temporary database
|
||||
async fn test_storage() -> (Arc<Storage>, TempDir) {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let storage = Storage::new(Some(dir.path().join("test.db"))).unwrap();
|
||||
(Arc::new(storage), dir)
|
||||
}
|
||||
|
||||
/// Helper to ingest test content
|
||||
async fn ingest_test_content(storage: &Arc<Storage>, content: &str) -> String {
|
||||
let input = IngestInput {
|
||||
content: content.to_string(),
|
||||
node_type: "fact".to_string(),
|
||||
source: None,
|
||||
sentiment_score: 0.0,
|
||||
sentiment_magnitude: 0.0,
|
||||
tags: vec![],
|
||||
valid_from: None,
|
||||
valid_until: None,
|
||||
};
|
||||
let node = storage.ingest(input).unwrap();
|
||||
node.id
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// QUERY VALIDATION TESTS
|
||||
// ========================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_recall_empty_query_fails() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
let args = serde_json::json!({ "query": "" });
|
||||
let result = execute(&storage, Some(args)).await;
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("empty"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_recall_whitespace_only_query_fails() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
let args = serde_json::json!({ "query": " \t\n " });
|
||||
let result = execute(&storage, Some(args)).await;
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("empty"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_recall_missing_arguments_fails() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
let result = execute(&storage, None).await;
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("Missing arguments"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_recall_missing_query_field_fails() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
let args = serde_json::json!({ "limit": 10 });
|
||||
let result = execute(&storage, Some(args)).await;
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("Invalid arguments"));
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// LIMIT CLAMPING TESTS
|
||||
// ========================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_recall_limit_clamped_to_minimum() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
// Ingest some content first
|
||||
ingest_test_content(&storage, "Test content for limit clamping").await;
|
||||
|
||||
// Try with limit 0 - should clamp to 1
|
||||
let args = serde_json::json!({
|
||||
"query": "test",
|
||||
"limit": 0
|
||||
});
|
||||
let result = execute(&storage, Some(args)).await;
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_recall_limit_clamped_to_maximum() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
// Ingest some content first
|
||||
ingest_test_content(&storage, "Test content for max limit").await;
|
||||
|
||||
// Try with limit 1000 - should clamp to 100
|
||||
let args = serde_json::json!({
|
||||
"query": "test",
|
||||
"limit": 1000
|
||||
});
|
||||
let result = execute(&storage, Some(args)).await;
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_recall_negative_limit_clamped() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
ingest_test_content(&storage, "Test content for negative limit").await;
|
||||
|
||||
let args = serde_json::json!({
|
||||
"query": "test",
|
||||
"limit": -5
|
||||
});
|
||||
let result = execute(&storage, Some(args)).await;
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// MIN_RETENTION CLAMPING TESTS
|
||||
// ========================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_recall_min_retention_clamped_to_zero() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
ingest_test_content(&storage, "Test content for retention clamping").await;
|
||||
|
||||
let args = serde_json::json!({
|
||||
"query": "test",
|
||||
"min_retention": -0.5
|
||||
});
|
||||
let result = execute(&storage, Some(args)).await;
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_recall_min_retention_clamped_to_one() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
ingest_test_content(&storage, "Test content for max retention").await;
|
||||
|
||||
let args = serde_json::json!({
|
||||
"query": "test",
|
||||
"min_retention": 1.5
|
||||
});
|
||||
let result = execute(&storage, Some(args)).await;
|
||||
// Should succeed but return no results (retention > 1.0 clamped to 1.0)
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// SUCCESSFUL RECALL TESTS
|
||||
// ========================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_recall_basic_query_succeeds() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
ingest_test_content(&storage, "The Rust programming language is memory safe.").await;
|
||||
|
||||
let args = serde_json::json!({ "query": "rust" });
|
||||
let result = execute(&storage, Some(args)).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let value = result.unwrap();
|
||||
assert_eq!(value["query"], "rust");
|
||||
assert!(value["total"].is_number());
|
||||
assert!(value["results"].is_array());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_recall_returns_matching_content() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
let node_id =
|
||||
ingest_test_content(&storage, "Python is a dynamic programming language.").await;
|
||||
|
||||
let args = serde_json::json!({ "query": "python" });
|
||||
let result = execute(&storage, Some(args)).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let value = result.unwrap();
|
||||
let results = value["results"].as_array().unwrap();
|
||||
assert!(!results.is_empty());
|
||||
assert_eq!(results[0]["id"], node_id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_recall_with_limit() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
// Ingest multiple items
|
||||
ingest_test_content(&storage, "Testing content one").await;
|
||||
ingest_test_content(&storage, "Testing content two").await;
|
||||
ingest_test_content(&storage, "Testing content three").await;
|
||||
|
||||
let args = serde_json::json!({
|
||||
"query": "testing",
|
||||
"limit": 2
|
||||
});
|
||||
let result = execute(&storage, Some(args)).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let value = result.unwrap();
|
||||
let results = value["results"].as_array().unwrap();
|
||||
assert!(results.len() <= 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_recall_empty_database_returns_empty_array() {
|
||||
// With hybrid search (keyword + semantic), any query against content
|
||||
// may return low-similarity matches. The true "no matches" case
|
||||
// is an empty database.
|
||||
let (storage, _dir) = test_storage().await;
|
||||
// Don't ingest anything - database is empty
|
||||
|
||||
let args = serde_json::json!({ "query": "anything" });
|
||||
let result = execute(&storage, Some(args)).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let value = result.unwrap();
|
||||
assert_eq!(value["total"], 0);
|
||||
assert!(value["results"].as_array().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_recall_result_contains_expected_fields() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
ingest_test_content(&storage, "Testing field presence in recall results.").await;
|
||||
|
||||
let args = serde_json::json!({ "query": "testing" });
|
||||
let result = execute(&storage, Some(args)).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let value = result.unwrap();
|
||||
let results = value["results"].as_array().unwrap();
|
||||
if !results.is_empty() {
|
||||
let first = &results[0];
|
||||
assert!(first["id"].is_string());
|
||||
assert!(first["content"].is_string());
|
||||
assert!(first["nodeType"].is_string());
|
||||
assert!(first["retentionStrength"].is_number());
|
||||
assert!(first["stability"].is_number());
|
||||
assert!(first["difficulty"].is_number());
|
||||
assert!(first["reps"].is_number());
|
||||
assert!(first["createdAt"].is_string());
|
||||
assert!(first["lastAccessed"].is_string());
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// DEFAULT VALUES TESTS
|
||||
// ========================================================================
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_recall_default_limit_is_10() {
|
||||
let (storage, _dir) = test_storage().await;
|
||||
// Ingest more than 10 items
|
||||
for i in 0..15 {
|
||||
ingest_test_content(&storage, &format!("Item number {}", i)).await;
|
||||
}
|
||||
|
||||
let args = serde_json::json!({ "query": "item" });
|
||||
let result = execute(&storage, Some(args)).await;
|
||||
assert!(result.is_ok());
|
||||
|
||||
let value = result.unwrap();
|
||||
let results = value["results"].as_array().unwrap();
|
||||
assert!(results.len() <= 10);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// SCHEMA TESTS
|
||||
// ========================================================================
|
||||
|
||||
#[test]
|
||||
fn test_schema_has_required_fields() {
|
||||
let schema_value = schema();
|
||||
assert_eq!(schema_value["type"], "object");
|
||||
assert!(schema_value["properties"]["query"].is_object());
|
||||
assert!(
|
||||
schema_value["required"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.contains(&serde_json::json!("query"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_schema_has_optional_fields() {
|
||||
let schema_value = schema();
|
||||
assert!(schema_value["properties"]["limit"].is_object());
|
||||
assert!(schema_value["properties"]["min_retention"].is_object());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_schema_limit_has_bounds() {
|
||||
let schema_value = schema();
|
||||
let limit_schema = &schema_value["properties"]["limit"];
|
||||
assert_eq!(limit_schema["minimum"], 1);
|
||||
assert_eq!(limit_schema["maximum"], 100);
|
||||
assert_eq!(limit_schema["default"], 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_schema_min_retention_has_bounds() {
|
||||
let schema_value = schema();
|
||||
let retention_schema = &schema_value["properties"]["min_retention"];
|
||||
assert_eq!(retention_schema["minimum"], 0.0);
|
||||
assert_eq!(retention_schema["maximum"], 1.0);
|
||||
assert_eq!(retention_schema["default"], 0.0);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
//! Search Tools (Deprecated - use search_unified instead)
|
||||
//!
|
||||
//! Semantic and hybrid search implementations.
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
|
||||
use vestige_core::Storage;
|
||||
|
||||
/// Input schema for semantic_search tool
|
||||
pub fn semantic_schema() -> Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Search query for semantic similarity"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of results (default: 10)",
|
||||
"default": 10,
|
||||
"minimum": 1,
|
||||
"maximum": 50
|
||||
},
|
||||
"min_similarity": {
|
||||
"type": "number",
|
||||
"description": "Minimum similarity threshold (0.0-1.0, default: 0.5)",
|
||||
"default": 0.5,
|
||||
"minimum": 0.0,
|
||||
"maximum": 1.0
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
})
|
||||
}
|
||||
|
||||
/// Input schema for hybrid_search tool
|
||||
pub fn hybrid_schema() -> Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Search query"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of results (default: 10)",
|
||||
"default": 10,
|
||||
"minimum": 1,
|
||||
"maximum": 50
|
||||
},
|
||||
"keyword_weight": {
|
||||
"type": "number",
|
||||
"description": "Weight for keyword search (0.0-1.0, default: 0.5)",
|
||||
"default": 0.5,
|
||||
"minimum": 0.0,
|
||||
"maximum": 1.0
|
||||
},
|
||||
"semantic_weight": {
|
||||
"type": "number",
|
||||
"description": "Weight for semantic search (0.0-1.0, default: 0.5)",
|
||||
"default": 0.5,
|
||||
"minimum": 0.0,
|
||||
"maximum": 1.0
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SemanticSearchArgs {
|
||||
query: String,
|
||||
limit: Option<i32>,
|
||||
min_similarity: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct HybridSearchArgs {
|
||||
query: String,
|
||||
limit: Option<i32>,
|
||||
keyword_weight: Option<f32>,
|
||||
semantic_weight: Option<f32>,
|
||||
}
|
||||
|
||||
pub async fn execute_semantic(
|
||||
storage: &Arc<Storage>,
|
||||
args: Option<Value>,
|
||||
) -> Result<Value, String> {
|
||||
let args: SemanticSearchArgs = match args {
|
||||
Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?,
|
||||
None => return Err("Missing arguments".to_string()),
|
||||
};
|
||||
|
||||
if args.query.trim().is_empty() {
|
||||
return Err("Query cannot be empty".to_string());
|
||||
}
|
||||
|
||||
// Check if embeddings are ready
|
||||
if !storage.is_embedding_ready() {
|
||||
return Ok(serde_json::json!({
|
||||
"error": "Embedding service not ready",
|
||||
"hint": "Run consolidation first to initialize embeddings, or the model may still be loading.",
|
||||
}));
|
||||
}
|
||||
|
||||
let results = storage
|
||||
.semantic_search(
|
||||
&args.query,
|
||||
args.limit.unwrap_or(10).clamp(1, 50),
|
||||
args.min_similarity.unwrap_or(0.5).clamp(0.0, 1.0),
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let formatted: Vec<Value> = results
|
||||
.iter()
|
||||
.map(|r| {
|
||||
serde_json::json!({
|
||||
"id": r.node.id,
|
||||
"content": r.node.content,
|
||||
"similarity": r.similarity,
|
||||
"nodeType": r.node.node_type,
|
||||
"tags": r.node.tags,
|
||||
"retentionStrength": r.node.retention_strength,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"query": args.query,
|
||||
"method": "semantic",
|
||||
"total": formatted.len(),
|
||||
"results": formatted,
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn execute_hybrid(storage: &Arc<Storage>, args: Option<Value>) -> Result<Value, String> {
|
||||
let args: HybridSearchArgs = match args {
|
||||
Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?,
|
||||
None => return Err("Missing arguments".to_string()),
|
||||
};
|
||||
|
||||
if args.query.trim().is_empty() {
|
||||
return Err("Query cannot be empty".to_string());
|
||||
}
|
||||
|
||||
let results = storage
|
||||
.hybrid_search(
|
||||
&args.query,
|
||||
args.limit.unwrap_or(10).clamp(1, 50),
|
||||
args.keyword_weight.unwrap_or(0.3).clamp(0.0, 1.0),
|
||||
args.semantic_weight.unwrap_or(0.7).clamp(0.0, 1.0),
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let formatted: Vec<Value> = results
|
||||
.iter()
|
||||
.map(|r| {
|
||||
serde_json::json!({
|
||||
"id": r.node.id,
|
||||
"content": r.node.content,
|
||||
"combinedScore": r.combined_score,
|
||||
"keywordScore": r.keyword_score,
|
||||
"semanticScore": r.semantic_score,
|
||||
"matchType": format!("{:?}", r.match_type),
|
||||
"nodeType": r.node.node_type,
|
||||
"tags": r.node.tags,
|
||||
"retentionStrength": r.node.retention_strength,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"query": args.query,
|
||||
"method": "hybrid",
|
||||
"total": formatted.len(),
|
||||
"results": formatted,
|
||||
}))
|
||||
}
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
//! Stats Tools (Deprecated - use memory_unified instead)
|
||||
//!
|
||||
//! Memory statistics and health check.
|
||||
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
|
||||
use vestige_core::{MemoryStats, Storage};
|
||||
|
||||
/// Input schema for get_stats tool
|
||||
pub fn stats_schema() -> Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
})
|
||||
}
|
||||
|
||||
/// Input schema for health_check tool
|
||||
pub fn health_schema() -> Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn execute_stats(storage: &Arc<Storage>) -> Result<Value, String> {
|
||||
let stats = storage.get_stats().map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"totalNodes": stats.total_nodes,
|
||||
"nodesDueForReview": stats.nodes_due_for_review,
|
||||
"averageRetention": stats.average_retention,
|
||||
"averageStorageStrength": stats.average_storage_strength,
|
||||
"averageRetrievalStrength": stats.average_retrieval_strength,
|
||||
"oldestMemory": stats.oldest_memory.map(|d| d.to_rfc3339()),
|
||||
"newestMemory": stats.newest_memory.map(|d| d.to_rfc3339()),
|
||||
"nodesWithEmbeddings": stats.nodes_with_embeddings,
|
||||
"embeddingModel": stats.embedding_model,
|
||||
"embeddingServiceReady": storage.is_embedding_ready(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn execute_health(storage: &Arc<Storage>) -> Result<Value, String> {
|
||||
let stats = storage.get_stats().map_err(|e| e.to_string())?;
|
||||
|
||||
// Determine health status
|
||||
let status = if stats.total_nodes == 0 {
|
||||
"empty"
|
||||
} else if stats.average_retention < 0.3 {
|
||||
"critical"
|
||||
} else if stats.average_retention < 0.5 {
|
||||
"degraded"
|
||||
} else {
|
||||
"healthy"
|
||||
};
|
||||
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
if stats.average_retention < 0.5 && stats.total_nodes > 0 {
|
||||
warnings.push(
|
||||
"Low average retention - consider running consolidation or reviewing memories"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if stats.nodes_due_for_review > 10 {
|
||||
warnings.push(format!(
|
||||
"{} memories are due for review",
|
||||
stats.nodes_due_for_review
|
||||
));
|
||||
}
|
||||
|
||||
if stats.total_nodes > 0 && stats.nodes_with_embeddings == 0 {
|
||||
warnings.push(
|
||||
"No embeddings generated - semantic search unavailable. Run consolidation.".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let embedding_coverage = if stats.total_nodes > 0 {
|
||||
(stats.nodes_with_embeddings as f64 / stats.total_nodes as f64) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
if embedding_coverage < 50.0 && stats.total_nodes > 10 {
|
||||
warnings.push(format!(
|
||||
"Only {:.1}% of memories have embeddings",
|
||||
embedding_coverage
|
||||
));
|
||||
}
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"status": status,
|
||||
"totalNodes": stats.total_nodes,
|
||||
"nodesDueForReview": stats.nodes_due_for_review,
|
||||
"averageRetention": stats.average_retention,
|
||||
"embeddingCoverage": format!("{:.1}%", embedding_coverage),
|
||||
"embeddingServiceReady": storage.is_embedding_ready(),
|
||||
"warnings": warnings,
|
||||
"recommendations": get_recommendations(&stats, status),
|
||||
}))
|
||||
}
|
||||
|
||||
fn get_recommendations(stats: &MemoryStats, status: &str) -> Vec<String> {
|
||||
let mut recommendations = Vec::new();
|
||||
|
||||
if status == "critical" {
|
||||
recommendations.push("CRITICAL: Many memories have very low retention. Review important memories with 'mark_reviewed'.".to_string());
|
||||
}
|
||||
|
||||
if stats.nodes_due_for_review > 5 {
|
||||
recommendations.push("Review due memories to strengthen retention.".to_string());
|
||||
}
|
||||
|
||||
if stats.nodes_with_embeddings < stats.total_nodes {
|
||||
recommendations.push(
|
||||
"Run 'run_consolidation' to generate embeddings for better semantic search."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if stats.total_nodes > 100 && stats.average_retention < 0.7 {
|
||||
recommendations
|
||||
.push("Consider running periodic consolidation to maintain memory health.".to_string());
|
||||
}
|
||||
|
||||
if recommendations.is_empty() {
|
||||
recommendations.push("Memory system is healthy!".to_string());
|
||||
}
|
||||
|
||||
recommendations
|
||||
}
|
||||
|
|
@ -31,10 +31,16 @@ export FASTEMBED_CACHE_PATH="/custom/path"
|
|||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `VESTIGE_DATA_DIR` | Platform default | Custom database location |
|
||||
| `VESTIGE_LOG_LEVEL` | `info` | Logging verbosity |
|
||||
| `RUST_LOG` | - | Detailed tracing output |
|
||||
| `RUST_LOG` | `info` (via tracing-subscriber) | Log verbosity + per-module filtering |
|
||||
| `FASTEMBED_CACHE_PATH` | `./.fastembed_cache` | Embedding model cache location |
|
||||
| `VESTIGE_DASHBOARD_PORT` | `3927` | Dashboard HTTP + WebSocket port |
|
||||
| `VESTIGE_HTTP_PORT` | `3928` | Optional MCP-over-HTTP port |
|
||||
| `VESTIGE_HTTP_BIND` | `127.0.0.1` | HTTP bind address |
|
||||
| `VESTIGE_AUTH_TOKEN` | auto-generated | Dashboard + MCP HTTP bearer auth |
|
||||
| `VESTIGE_DASHBOARD_ENABLED` | `true` | Set `false` to disable the web dashboard |
|
||||
| `VESTIGE_CONSOLIDATION_INTERVAL_HOURS` | `6` | FSRS-6 decay cycle cadence |
|
||||
|
||||
> **Storage location** is controlled by the `--data-dir <path>` CLI flag (see below), not an env var. Default is your OS's per-user data directory: `~/Library/Application Support/com.vestige.core/` on macOS, `~/.local/share/vestige/` on Linux, `%APPDATA%\vestige\core\data\` on Windows.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -396,7 +396,6 @@ Binary crate. Wraps `vestige-core` behind an MCP JSON-RPC 2.0 server, plus an em
|
|||
|
||||
| Var | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `VESTIGE_DATA_DIR` | `~/.vestige/` | Storage root |
|
||||
| `VESTIGE_DASHBOARD_PORT` | `3927` | Dashboard HTTP + WebSocket port |
|
||||
| `VESTIGE_HTTP_PORT` | `3928` | Optional MCP-over-HTTP port |
|
||||
| `VESTIGE_HTTP_BIND` | `127.0.0.1` | HTTP bind address |
|
||||
|
|
@ -706,7 +705,7 @@ vestige-cloud/
|
|||
├── Cargo.toml # binary: vestige-http
|
||||
└── src/
|
||||
├── main.rs # Axum server on :3927, auth + cors middleware
|
||||
├── auth.rs # Single bearer token via VESTIGE_API_KEY env, open if unset
|
||||
├── auth.rs # Single bearer token via VESTIGE_AUTH_TOKEN env (auto-generated if unset, stored in data-dir)
|
||||
├── cors.rs # prod: allowlist vestige.dev + app.vestige.dev; dev: permissive
|
||||
├── state.rs # Arc<Mutex<Storage>> shared state (SINGLE TENANT)
|
||||
├── sse.rs # /mcp/sse STUB — 3 TODOs, returns one static "endpoint" event
|
||||
|
|
|
|||
|
|
@ -91,9 +91,12 @@ export FASTEMBED_CACHE_PATH="$HOME/.fastembed_cache"
|
|||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `VESTIGE_DATA_DIR` | Data storage directory | `~/.vestige` |
|
||||
| `VESTIGE_LOG_LEVEL` | Log verbosity | `info` |
|
||||
| `FASTEMBED_CACHE_PATH` | Embeddings model location | `./.fastembed_cache` |
|
||||
| `RUST_LOG` | Log verbosity + per-module filter | `info` |
|
||||
| `FASTEMBED_CACHE_PATH` | Embeddings model cache | `./.fastembed_cache` |
|
||||
| `VESTIGE_DASHBOARD_PORT` | Dashboard port | `3927` |
|
||||
| `VESTIGE_AUTH_TOKEN` | Bearer auth for dashboard + HTTP MCP | auto-generated |
|
||||
|
||||
Storage location is the `--data-dir <path>` CLI flag (defaults to your OS's per-user data directory).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue