feat(server): refactor handlers, router, state management, and memory tools

This commit is contained in:
Riz Ashraf committed 2026-10-02 07:27:37 +01:00
1 parent 87ddb01063
commit a083719cf1
36 files changed
+1899 -597

No files matched your search

+208 -88
View File
@@ -39,9 +39,9 @@ impl McpTool for LogDecisionHandler {
final_id = format!("ADR-{:04}", adrs.len() + 1);
let a = Adr {
id: final_id.clone(),
title: req.title,
title: req.title.clone(),
context: req.context,
decision: req.decision,
decision: req.decision.clone(),
consequence: req.consequence,
status: "accepted".to_string(),
supersedes: req.supersedes,
@@ -52,7 +52,8 @@ impl McpTool for LogDecisionHandler {
adrs.push(a);
});
Ok(format!("Decision logged as {}", final_id))
state.record_activity("decision", &format!("Logged {}: {}", final_id, req.title), Some(&req.decision));
Ok(format!("Logged decision {}: {}", final_id, req.title))
}
}
@@ -119,7 +120,9 @@ impl McpTool for DeleteDecisionHandler {
state.rebuild_index().await;
Ok("Decision deleted successfully".to_string())
} else {
Err(crate::error::AppError::Internal("Decision not found".to_string()))
Err(crate::error::AppError::Internal(
"Decision not found".to_string(),
))
}
}
}
@@ -139,18 +142,40 @@ impl McpTool for LogErrorFixHandler {
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: LogErrorFixTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let text_to_embed = format!("Signature: {}\nSolution: {}", req.signature, req.solution);
let embedding = crate::embedding::generate_embedding_async(text_to_embed).await.ok();
let mut solution = req.solution;
if state.ollama.is_available().await {
let prompt = format!(
"Analyze this error signature and solution. Output 1 sentence summarizing the root cause and fix:\nSignature: {}\nSolution: {}",
req.signature, solution
);
if let Ok(summary) = state
.ollama
.generate(&prompt, Some(&state.ollama.reasoning_model), None)
.await
{
let clean = summary.trim();
if !clean.is_empty() {
solution = format!("{} (AI Analysis: {})", solution, clean);
}
}
}
let embedding = crate::embedding::generate_embedding_async(text_to_embed)
.await
.ok();
state.code.error_fixes.modify(|fixes| {
fixes.push(crate::models::ErrorFix {
signature: req.signature,
solution: req.solution,
signature: req.signature.clone(),
solution: solution.clone(),
timestamp: crate::handlers::utils::now_secs(),
git_commit: req.git_commit,
git_branch: req.git_branch,
embedding,
})
});
Ok("Error fix logged".to_string())
state.record_activity("error_fix", &format!("Fixed error: {}", req.signature), Some(&solution));
Ok(format!("Logged error fix for {}: {}", req.signature, solution))
}
}
@@ -200,16 +225,32 @@ impl McpTool for LogCodeChangeHandler {
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: LogCodeChangeTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut description = req.description;
if state.ollama.is_available().await {
let prompt = format!(
"Summarize in 1 concise sentence the architectural impact of changing file '{}': {}",
req.file_path, description
);
if let Ok(summary) = state.ollama.generate(&prompt, None, None).await {
let clean = summary.trim();
if !clean.is_empty() {
description = format!("{} (AI Summary: {})", description, clean);
}
}
}
state.code.ledger.modify(|ledger| {
ledger.push(CodeChange {
timestamp: crate::handlers::utils::now_secs(),
file_path: req.file_path,
description: req.description,
file_path: req.file_path.clone(),
description: description.clone(),
git_commit: req.git_commit,
git_branch: req.git_branch,
});
});
Ok("Code change logged".to_string())
state.record_activity("code_change", &format!("Modified {}", req.file_path), Some(&description));
Ok(format!("Logged code change for {}: {}", req.file_path, description))
}
}
@@ -230,7 +271,8 @@ impl McpTool for QueryRecentChangesHandler {
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let data = state
.code.ledger
.code
.ledger
.read_with(|l| Ok::<String, crate::error::AppError>(serde_json::to_string(l)?))?;
Ok(data)
}
@@ -278,7 +320,8 @@ impl McpTool for ReadPreferencesHandler {
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
state
.env.prefs
.env
.prefs
.read_with(|prefs| Ok::<String, crate::error::AppError>(serde_json::to_string(prefs)?))
}
}
@@ -297,8 +340,13 @@ impl McpTool for LogTechDebtHandler {
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: LogTechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let text_to_embed = format!("Description: {}\nIdeal Solution: {}", req.description, req.ideal_solution);
let embedding = crate::embedding::generate_embedding_async(text_to_embed).await.ok();
let text_to_embed = format!(
"Description: {}\nIdeal Solution: {}",
req.description, req.ideal_solution
);
let embedding = crate::embedding::generate_embedding_async(text_to_embed)
.await
.ok();
state.code.tech_debts.modify(|debts| {
debts.push(crate::models::TechDebt {
id: uuid::Uuid::new_v4().to_string(),
@@ -346,7 +394,10 @@ impl McpTool for ResolveTechDebtHandler {
if found {
Ok("Tech debt resolved".to_string())
} else {
Err(crate::error::AppError::Internal("Tech debt not found. Please verify the tech debt ID using list_tech_debt.".to_string()))
Err(crate::error::AppError::Internal(
"Tech debt not found. Please verify the tech debt ID using list_tech_debt."
.to_string(),
))
}
}
}
@@ -394,10 +445,15 @@ impl McpTool for OmniSearchHandler {
let req: OmniSearchTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let limit = req.limit.unwrap_or(5);
let include_body = req.include_body.unwrap_or(false);
let matches = state.search().keyword_search(&req.query, req.namespace.as_deref(), limit).unwrap_or_default();
let matches = state
.search()
.keyword_search(&req.query, req.namespace.as_deref(), limit)
.unwrap_or_default();
// tracing::info!("OMNI SEARCH MATCHES: {:?}", matches);
let q = req.query.clone();
let query_emb = crate::embedding::generate_embedding_async(q.clone()).await.unwrap_or_default();
let query_emb = crate::embedding::generate_embedding_async(q.clone())
.await
.unwrap_or_default();
let kg_json = state.read_graph(|full| {
let mut kg_entities = std::collections::HashMap::new();
@@ -460,27 +516,38 @@ impl McpTool for OmniSearchHandler {
})?;
let snippets_json = state.code.snippets.read_with(|all_snippets| {
let mut scored: Vec<_> = all_snippets.iter().map(|s| {
let mut score = 0.0;
if matched_snippets.contains(s.name.as_str()) || contains_ignore_ascii_case(&s.name, &q) || contains_ignore_ascii_case(&s.description, &q) {
score += 1.0;
}
if let Some(emb) = &s.embedding {
score += crate::embedding::cosine_similarity(&query_emb, emb);
}
(s, score)
}).filter(|(_, score)| *score > 0.4).collect();
let mut scored: Vec<_> = all_snippets
.iter()
.map(|s| {
let mut score = 0.0;
if matched_snippets.contains(s.name.as_str())
|| contains_ignore_ascii_case(&s.name, &q)
|| contains_ignore_ascii_case(&s.description, &q)
{
score += 1.0;
}
if let Some(emb) = &s.embedding {
score += crate::embedding::cosine_similarity(&query_emb, emb);
}
(s, score)
})
.filter(|(_, score)| *score > 0.4)
.collect();
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let filtered: Vec<_> = scored.into_iter().take(limit).map(|(s, _)| {
if !include_body {
let mut summary = s.clone();
summary.code = "".to_string();
summary
} else {
s.clone()
}
}).collect();
let filtered: Vec<_> = scored
.into_iter()
.take(limit)
.map(|(s, _)| {
if !include_body {
let mut summary = s.clone();
summary.code = "".to_string();
summary
} else {
s.clone()
}
})
.collect();
serde_json::to_value(&filtered).map_err(|e| e.to_string())
})?;
@@ -506,31 +573,41 @@ impl McpTool for OmniSearchHandler {
let q = req.query;
let tech_debts_json = state.code.tech_debts.read_with(|debts| {
let mut scored: Vec<_> = debts.iter().map(|d| {
if req.namespace.as_ref().is_some_and(|ns| d.namespace != *ns) {
return (d, 0.0);
}
let mut score = 0.0;
if contains_ignore_ascii_case(&d.description, &q) || contains_ignore_ascii_case(&d.ideal_solution, &q) {
score += 1.0;
}
if let Some(emb) = &d.embedding {
score += crate::embedding::cosine_similarity(&query_emb, emb);
}
(d, score)
}).filter(|(_, score)| *score > 0.4).collect();
let mut scored: Vec<_> = debts
.iter()
.map(|d| {
if req.namespace.as_ref().is_some_and(|ns| d.namespace != *ns) {
return (d, 0.0);
}
let mut score = 0.0;
if contains_ignore_ascii_case(&d.description, &q)
|| contains_ignore_ascii_case(&d.ideal_solution, &q)
{
score += 1.0;
}
if let Some(emb) = &d.embedding {
score += crate::embedding::cosine_similarity(&query_emb, emb);
}
(d, score)
})
.filter(|(_, score)| *score > 0.4)
.collect();
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let filtered: Vec<_> = scored.into_iter().take(limit).map(|(d, _)| {
if !include_body {
let mut summary = d.clone();
summary.description = "".to_string();
summary.ideal_solution = "".to_string();
summary
} else {
d.clone()
}
}).collect();
let filtered: Vec<_> = scored
.into_iter()
.take(limit)
.map(|(d, _)| {
if !include_body {
let mut summary = d.clone();
summary.description = "".to_string();
summary.ideal_solution = "".to_string();
summary
} else {
d.clone()
}
})
.collect();
serde_json::to_value(&filtered).map_err(|e| e.to_string())
})?;
@@ -556,19 +633,29 @@ impl McpTool for OmniSearchHandler {
})?;
let error_fixes_json = state.code.error_fixes.read_with(|fixes| {
let mut scored: Vec<_> = fixes.iter().map(|f| {
let mut score = 0.0;
if contains_ignore_ascii_case(&f.signature, &q) || contains_ignore_ascii_case(&f.solution, &q) {
score += 1.0;
}
if let Some(emb) = &f.embedding {
score += crate::embedding::cosine_similarity(&query_emb, emb);
}
(f, score)
}).filter(|(_, score)| *score > 0.4).collect();
let mut scored: Vec<_> = fixes
.iter()
.map(|f| {
let mut score = 0.0;
if contains_ignore_ascii_case(&f.signature, &q)
|| contains_ignore_ascii_case(&f.solution, &q)
{
score += 1.0;
}
if let Some(emb) = &f.embedding {
score += crate::embedding::cosine_similarity(&query_emb, emb);
}
(f, score)
})
.filter(|(_, score)| *score > 0.4)
.collect();
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let filtered: Vec<_> = scored.into_iter().take(limit).map(|(f, _)| f.clone()).collect();
let filtered: Vec<_> = scored
.into_iter()
.take(limit)
.map(|(f, _)| f.clone())
.collect();
serde_json::to_value(&filtered).map_err(|e| e.to_string())
})?;
@@ -603,7 +690,8 @@ impl McpTool for GetProjectHealthHandler {
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: GetProjectHealthTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let active_tasks = state
.project.tasks
.project
.tasks
.read_with(|tasks| tasks.iter().filter(|t| t.status != "done").count());
let unresolved_debt = state.code.tech_debts.read_with(|debts| {
debts
@@ -663,8 +751,12 @@ mod tests {
"git_branch": "main"
});
let res = handler.execute(args, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
assert!(res.contains("Error fix logged"));
let res = handler
.execute(args, state.clone())
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
assert!(res.contains("Logged error fix"));
}
#[tokio::test]
@@ -675,7 +767,11 @@ mod tests {
let handler = GetProjectHealthHandler;
let args = json!({"namespace": "global"});
let res = handler.execute(args, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
let res = handler
.execute(args, state.clone())
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
assert!(res.contains("unresolved_tech_debt"));
}
@@ -693,8 +789,10 @@ mod tests {
});
let res1 = decision_handler
.execute(args_dec, state.clone())
.await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
assert_eq!(res1, "Decision logged as ADR-0001");
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
assert_eq!(res1, "Logged decision ADR-0001: Architecture");
let debt_handler = LogTechDebtHandler;
let args_debt = json!({
@@ -709,7 +807,9 @@ mod tests {
});
let res2 = debt_handler
.execute(args_debt, state.clone())
.await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
assert_eq!(res2, "Tech debt logged");
let list_debt = ListTechDebtHandler;
@@ -718,7 +818,9 @@ mod tests {
json!({"namespace": "global", "include_resolved": false}),
state.clone(),
)
.await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
assert!(res3.contains("Hardcoded path"));
let pref_handler = LearnPreferenceHandler;
@@ -728,11 +830,17 @@ mod tests {
});
let res4 = pref_handler
.execute(args_pref, state.clone())
.await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
assert_eq!(res4, "Preference learned");
let read_pref = ReadPreferencesHandler;
let res5 = read_pref.execute(json!({}), state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
let res5 = read_pref
.execute(json!({}), state.clone())
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
assert!(res5.contains("use spaces"));
}
@@ -750,12 +858,16 @@ mod tests {
});
code_handler
.execute(args_code, state.clone())
.await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
let query_changes = QueryRecentChangesHandler;
let res_changes = query_changes
.execute(json!({}), state.clone())
.await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
assert!(res_changes.contains("main.rs"));
let debt_handler = LogTechDebtHandler;
@@ -771,7 +883,9 @@ mod tests {
});
debt_handler
.execute(args_debt, state.clone())
.await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
// resolve it
let list_debt = ListTechDebtHandler;
@@ -780,14 +894,18 @@ mod tests {
json!({"namespace": "global", "include_resolved": false}),
state.clone(),
)
.await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
let uuid_start = debt_list.find("id\":\"").unwrap() + 5;
let uuid = &debt_list[uuid_start..uuid_start + 36];
let resolve_debt = ResolveTechDebtHandler;
resolve_debt
.execute(json!({"id": uuid}), state.clone())
.await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
}
#[tokio::test]
@@ -821,7 +939,9 @@ mod tests {
let omni = OmniSearchHandler;
let omni_res = omni
.execute(json!({"query": "Omni"}), state.clone())
.await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
// tracing::info!("OMNI RES: {}", omni_res);
assert!(
omni_res.contains("omni-1"),