feat(embedding,vision): candle embeddings with offline fallback, on-demand clipboard vision capture, and concurrency audit

This commit is contained in:
Riz Ashraf committed 2026-10-07 06:36:09 +01:00
1 parent 5bd8b1587a
commit e4a0fe72df
47 files changed
+6292 -3503

No files matched your search

+240 -47
View File
@@ -113,11 +113,13 @@ impl McpResource for TasksActiveResource {
Some("List of currently active tasks")
}
async fn read(&self, state: Arc<MemoryState>) -> crate::error::Result<String> {
let tasks = state.project.tasks.cache.read().unwrap_or_else(|e| e.into_inner());
let data: Vec<_> = tasks
.iter()
.filter(|t| t.is_active())
.collect();
let tasks = state
.project
.tasks
.cache
.read()
.unwrap_or_else(|e| e.into_inner());
let data: Vec<_> = tasks.iter().filter(|t| t.is_active()).collect();
Ok(serde_json::to_string_pretty(&data)?)
}
}
@@ -135,7 +137,12 @@ impl McpResource for DecisionsActiveResource {
Some("All accepted Architectural Decision Records (ADRs)")
}
async fn read(&self, state: Arc<MemoryState>) -> crate::error::Result<String> {
let adrs = state.code.adrs.cache.read().unwrap_or_else(|e| e.into_inner());
let adrs = state
.code
.adrs
.cache
.read()
.unwrap_or_else(|e| e.into_inner());
let data: Vec<_> = adrs.iter().filter(|a| a.status == "accepted").collect();
Ok(serde_json::to_string_pretty(&data)?)
}
@@ -154,7 +161,12 @@ impl McpResource for TechDebtActiveResource {
Some("All currently unresolved technical debt items")
}
async fn read(&self, state: Arc<MemoryState>) -> crate::error::Result<String> {
let debts = state.code.tech_debts.cache.read().unwrap_or_else(|e| e.into_inner());
let debts = state
.code
.tech_debts
.cache
.read()
.unwrap_or_else(|e| e.into_inner());
let data: Vec<_> = debts.iter().filter(|d| !d.is_resolved).collect();
Ok(serde_json::to_string_pretty(&data)?)
}
@@ -233,14 +245,32 @@ impl McpResource for SessionDeltaResource {
let lookback = 7200; // last 2 hours
let cutoff = now.saturating_sub(lookback);
let changes = state.code.ledger.cache.read().unwrap_or_else(|e| e.into_inner());
let changes = state
.code
.ledger
.cache
.read()
.unwrap_or_else(|e| e.into_inner());
let recent_changes: Vec<_> = changes.iter().filter(|c| c.timestamp >= cutoff).collect();
let tasks = state.project.tasks.cache.read().unwrap_or_else(|e| e.into_inner());
let tasks = state
.project
.tasks
.cache
.read()
.unwrap_or_else(|e| e.into_inner());
let active_tasks: Vec<_> = tasks.iter().filter(|t| t.is_active()).collect();
let notes = state.code.sticky.cache.read().unwrap_or_else(|e| e.into_inner());
let active_notes: Vec<_> = notes.iter().filter(|n| n.expires_at.map_or(true, |exp| exp > now)).collect();
let notes = state
.code
.sticky
.cache
.read()
.unwrap_or_else(|e| e.into_inner());
let active_notes: Vec<_> = notes
.iter()
.filter(|n| n.expires_at.map_or(true, |exp| exp > now))
.collect();
let delta = serde_json::json!({
"recent_changes": recent_changes,
@@ -260,7 +290,9 @@ impl McpPrompt for ContextWarmupPrompt {
"context_warmup"
}
fn description(&self) -> Option<&'static str> {
Some("Consolidated context warmup prompt providing active tasks, pinned files, open tech debt, and sticky notes")
Some(
"Consolidated context warmup prompt providing active tasks, pinned files, open tech debt, and sticky notes",
)
}
async fn get(
&self,
@@ -334,7 +366,34 @@ impl MemoryHandler {
Some("Recent terminal execution history and exit codes")
}
async fn read(&self, state: Arc<MemoryState>) -> crate::error::Result<String> {
let items = state.telemetry.terminal_history.cache.read().unwrap_or_else(|e| e.into_inner());
let items = state
.telemetry
.terminal_history
.cache
.read()
.unwrap_or_else(|e| e.into_inner());
Ok(serde_json::to_string_pretty(&*items)?)
}
}
struct RecentActivitiesResource;
#[async_trait]
impl McpResource for RecentActivitiesResource {
fn uri(&self) -> &'static str {
"memory://activity/recent"
}
fn name(&self) -> &'static str {
"Recent Activity"
}
fn description(&self) -> Option<&'static str> {
Some("Recent IDE and developer activity logs")
}
async fn read(&self, state: Arc<MemoryState>) -> crate::error::Result<String> {
let items = state
.telemetry
.recent_activities
.cache
.read()
.unwrap_or_else(|e| e.into_inner());
Ok(serde_json::to_string_pretty(&*items)?)
}
}
@@ -351,7 +410,12 @@ impl MemoryHandler {
Some("Currently pinned files in the workspace")
}
async fn read(&self, state: Arc<MemoryState>) -> crate::error::Result<String> {
let items = state.project.pinned_files.cache.read().unwrap_or_else(|e| e.into_inner());
let items = state
.project
.pinned_files
.cache
.read()
.unwrap_or_else(|e| e.into_inner());
Ok(serde_json::to_string_pretty(&*items)?)
}
}
@@ -369,7 +433,12 @@ impl MemoryHandler {
Some("Project milestones and their status")
}
async fn read(&self, state: Arc<MemoryState>) -> crate::error::Result<String> {
let items = state.project.milestones.cache.read().unwrap_or_else(|e| e.into_inner());
let items = state
.project
.milestones
.cache
.read()
.unwrap_or_else(|e| e.into_inner());
Ok(serde_json::to_string_pretty(&*items)?)
}
}
@@ -403,6 +472,7 @@ impl MemoryHandler {
}
register_resource!(TerminalHistoryResource);
register_resource!(RecentActivitiesResource);
register_resource!(PinnedFilesResource);
register_resource!(MilestonesResource);
struct ArchiveRoutinePrompt;
@@ -679,7 +749,26 @@ impl MemoryHandler {
let category = match name {
"clipboard" => "CLIPBOARD",
"create_entities" | "create_relations" | "add_observations" | "delete_entities" | "delete_observations" | "delete_relations" | "read_graph" | "get_subgraph" | "search_graph" | "get_schema" | "search_nodes" | "open_nodes" | "visualize_graph" | "condense_entity" | "merge_entities" | "find_orphans" | "sweep_graph_health" | "summarize_subgraph" | "query_graph_path" | "resolve_stale_symbols" => "GRAPH",
"create_entities"
| "create_relations"
| "add_observations"
| "delete_entities"
| "delete_observations"
| "delete_relations"
| "read_graph"
| "get_subgraph"
| "search_graph"
| "get_schema"
| "search_nodes"
| "open_nodes"
| "visualize_graph"
| "condense_entity"
| "merge_entities"
| "find_orphans"
| "sweep_graph_health"
| "summarize_subgraph"
| "query_graph_path"
| "resolve_stale_symbols" => "GRAPH",
"decisions" => "DECISION",
"log_code_change" => "CODE",
"log_error_fix" => "ERROR_FIX",
@@ -701,7 +790,7 @@ impl MemoryHandler {
let result: crate::error::Result<String> = if let Some(tool) = self.tools.get(name)
{
tool.execute(args, self.state.clone()).await
execute_tool_span(name, tool.as_ref(), args, self.state.clone()).await
} else {
Err(crate::error::AppError::Internal(format!(
"Unknown tool: {}",
@@ -747,40 +836,90 @@ impl MemoryHandler {
pub fn format_tool_activity_description(name: &str, args: &serde_json::Value) -> String {
let (action, detail) = match name {
"tasks" => {
let act = args.get("action").and_then(|v| v.as_str()).unwrap_or("manage");
let title = args.get("title").or_else(|| args.get("id")).and_then(|v| v.as_str()).unwrap_or("");
("Tasks", format!("{}: {}", act, title).trim_end_matches(": ").to_string())
let act = args
.get("action")
.and_then(|v| v.as_str())
.unwrap_or("manage");
let title = args
.get("title")
.or_else(|| args.get("id"))
.and_then(|v| v.as_str())
.unwrap_or("");
(
"Tasks",
format!("{}: {}", act, title)
.trim_end_matches(": ")
.to_string(),
)
}
"decisions" => {
let act = args.get("action").and_then(|v| v.as_str()).unwrap_or("log");
let title = args.get("title").or_else(|| args.get("query")).and_then(|v| v.as_str()).unwrap_or("");
("Decisions", format!("{}: {}", act, title).trim_end_matches(": ").to_string())
let title = args
.get("title")
.or_else(|| args.get("query"))
.and_then(|v| v.as_str())
.unwrap_or("");
(
"Decisions",
format!("{}: {}", act, title)
.trim_end_matches(": ")
.to_string(),
)
}
"tech_debt" => {
let act = args.get("action").and_then(|v| v.as_str()).unwrap_or("log");
let desc = args.get("description").or_else(|| args.get("id")).and_then(|v| v.as_str()).unwrap_or("");
("Tech Debt", format!("{}: {}", act, desc).trim_end_matches(": ").to_string())
let desc = args
.get("description")
.or_else(|| args.get("id"))
.and_then(|v| v.as_str())
.unwrap_or("");
(
"Tech Debt",
format!("{}: {}", act, desc)
.trim_end_matches(": ")
.to_string(),
)
}
"sticky_notes" => {
let act = args.get("action").and_then(|v| v.as_str()).unwrap_or("add");
let preview = args.get("content").and_then(|v| v.as_str()).map(|c| c.chars().take(40).collect::<String>()).unwrap_or_default();
("Sticky Notes", format!("{}: {}", act, preview).trim_end_matches(": ").to_string())
let preview = args
.get("content")
.and_then(|v| v.as_str())
.map(|c| c.chars().take(40).collect::<String>())
.unwrap_or_default();
(
"Sticky Notes",
format!("{}: {}", act, preview)
.trim_end_matches(": ")
.to_string(),
)
}
"clipboard" => {
let act = args.get("action").and_then(|v| v.as_str()).unwrap_or("read");
let act = args
.get("action")
.and_then(|v| v.as_str())
.unwrap_or("read");
("Clipboard", act.to_string())
}
"snippets" => {
let act = args.get("action").and_then(|v| v.as_str()).unwrap_or("search");
let act = args
.get("action")
.and_then(|v| v.as_str())
.unwrap_or("search");
let q = args.get("query").and_then(|v| v.as_str()).unwrap_or("");
("Snippets", format!("{}: {}", act, q).trim_end_matches(": ").to_string())
(
"Snippets",
format!("{}: {}", act, q).trim_end_matches(": ").to_string(),
)
}
"log_code_change" => {
let file = args.get("file_path")
let file = args
.get("file_path")
.or_else(|| args.get("file"))
.or_else(|| args.get("target_file"))
.and_then(|v| v.as_str());
let summary = args.get("summary")
let summary = args
.get("summary")
.or_else(|| args.get("description"))
.and_then(|v| v.as_str());
@@ -793,7 +932,8 @@ pub fn format_tool_activity_description(name: &str, args: &serde_json::Value) ->
("Log Code Change", d)
}
"log_error_fix" => {
let d = args.get("error")
let d = args
.get("error")
.or_else(|| args.get("summary"))
.or_else(|| args.get("solution"))
.and_then(|v| v.as_str())
@@ -805,7 +945,11 @@ pub fn format_tool_activity_description(name: &str, args: &serde_json::Value) ->
let d = if let Some(entities) = args.get("entities").and_then(|v| v.as_array()) {
let names: Vec<&str> = entities
.iter()
.filter_map(|e| e.get("name").or_else(|| e.get("entity_name")).and_then(|n| n.as_str()))
.filter_map(|e| {
e.get("name")
.or_else(|| e.get("entity_name"))
.and_then(|n| n.as_str())
})
.take(3)
.collect();
if !names.is_empty() {
@@ -817,7 +961,11 @@ pub fn format_tool_activity_description(name: &str, args: &serde_json::Value) ->
} else {
format!("{} entity/entities", entities.len())
}
} else if let Some(name) = args.get("name").or_else(|| args.get("entity_name")).and_then(|v| v.as_str()) {
} else if let Some(name) = args
.get("name")
.or_else(|| args.get("entity_name"))
.and_then(|v| v.as_str())
{
name.to_string()
} else {
String::new()
@@ -829,7 +977,11 @@ pub fn format_tool_activity_description(name: &str, args: &serde_json::Value) ->
if let Some(first) = relations.first() {
let from = first.get("from").and_then(|v| v.as_str()).unwrap_or("?");
let to = first.get("to").and_then(|v| v.as_str()).unwrap_or("?");
let pred = first.get("relation_type").or_else(|| first.get("predicate")).and_then(|v| v.as_str()).unwrap_or("->");
let pred = first
.get("relation_type")
.or_else(|| first.get("predicate"))
.and_then(|v| v.as_str())
.unwrap_or("->");
if relations.len() > 1 {
format!("{} {} {} (+{} more)", from, pred, to, relations.len() - 1)
} else {
@@ -844,7 +996,8 @@ pub fn format_tool_activity_description(name: &str, args: &serde_json::Value) ->
("Create Relations", d)
}
"omni_search" | "search_graph" | "search_web" => {
let d = args.get("query")
let d = args
.get("query")
.and_then(|v| v.as_str())
.map(|q| format!("\"{}\"", q))
.unwrap_or_default();
@@ -881,6 +1034,16 @@ pub fn format_tool_activity_description(name: &str, args: &serde_json::Value) ->
}
}
#[tracing::instrument(name = "mcp_tool_execution", skip(tool, args, state), fields(tool_name = %name))]
async fn execute_tool_span(
name: &str,
tool: &dyn McpTool,
args: Value,
state: Arc<MemoryState>,
) -> crate::error::Result<String> {
tool.execute(args, state).await
}
#[cfg(test)]
mod tests {
use super::*;
@@ -889,10 +1052,16 @@ mod tests {
#[test]
fn test_format_tool_activity_description() {
let msg1 = format_tool_activity_description("log_code_change", &json!({ "file": "src/lib.rs", "summary": "Refactored library" }));
let msg1 = format_tool_activity_description(
"log_code_change",
&json!({ "file": "src/lib.rs", "summary": "Refactored library" }),
);
assert_eq!(msg1, "Log Code Change: src/lib.rs: Refactored library");
let msg2 = format_tool_activity_description("create_entities", &json!({ "entities": [{ "name": "EntityA" }, { "name": "EntityB" }] }));
let msg2 = format_tool_activity_description(
"create_entities",
&json!({ "entities": [{ "name": "EntityA" }, { "name": "EntityB" }] }),
);
assert_eq!(msg2, "Create Entities: EntityA, EntityB");
let msg3 = format_tool_activity_description("read_graph", &json!({}));
@@ -948,6 +1117,11 @@ mod tests {
.iter()
.any(|r| r["uri"] == "memory://pinned_files")
);
assert!(
resources_arr
.iter()
.any(|r| r["uri"] == "memory://activity/recent")
);
// Test resources/read
let req_read_res = json!({
@@ -1114,17 +1288,27 @@ mod tests {
assert_eq!(res["jsonrpc"], "2.0");
assert_eq!(res["id"], 100);
let tools = res["result"]["tools"].as_array().expect("tools array expected");
assert!(tools.len() >= 40, "Expected at least 40 registered MCP tools, found {}", tools.len());
let tools = res["result"]["tools"]
.as_array()
.expect("tools array expected");
assert!(
tools.len() >= 40,
"Expected at least 40 registered MCP tools, found {}",
tools.len()
);
for tool in tools {
let name = tool["name"].as_str().expect("tool name must be string");
assert!(!name.is_empty(), "Tool name cannot be empty");
let desc = tool["description"].as_str().expect("tool description must be string");
let desc = tool["description"]
.as_str()
.expect("tool description must be string");
assert!(!desc.is_empty(), "Tool '{}' missing description", name);
let schema = tool["inputSchema"].as_object().expect("inputSchema must be object");
let schema = tool["inputSchema"]
.as_object()
.expect("inputSchema must be object");
assert!(
schema.contains_key("properties") || schema.contains_key("type"),
"Tool '{}' schema must contain type/properties metadata",
@@ -1154,7 +1338,10 @@ mod tests {
}
});
let res = handler.handle_request(invalid_req).await.expect("handler must respond");
let res = handler
.handle_request(invalid_req)
.await
.expect("handler must respond");
assert_eq!(res["jsonrpc"], "2.0");
assert!(res["result"]["content"].is_array());
}
@@ -1180,7 +1367,10 @@ mod tests {
("sweep_graph_health", json!({})),
("get_next_actionable_tasks", json!({})),
("get_preflight_context", json!({})),
("auto_session_checkpoint", json!({"session_summary": "Test summary"})),
(
"auto_session_checkpoint",
json!({"session_summary": "Test summary"}),
),
];
for (name, args) in sample_valid_calls {
@@ -1194,13 +1384,16 @@ mod tests {
}
});
let res = handler.handle_request(valid_req).await.expect("handler must respond");
let res = handler
.handle_request(valid_req)
.await
.expect("handler must respond");
assert_eq!(res["jsonrpc"], "2.0");
assert_eq!(
res["result"]["isError"], false,
"Tool '{}' failed valid execution. Result: {:?}", name, res
"Tool '{}' failed valid execution. Result: {:?}",
name, res
);
}
}
}