Compare commits

..
Author SHA1 Message Date
Riz Ashraf eed9a9165c feat(graph): add ergonomic UpdateEntitiesTool for renaming and type changes 2026-10-10 12:27:29 +01:00
Riz Ashraf cfcd42f41e fix(graph): implement fuzzy substring matching for DeleteObservationsTool to improve LLM UX 2026-10-10 12:18:00 +01:00
Riz Ashraf 7c9e901c12 style: Apply cargo fix for unused variables 2026-10-10 11:12:03 +01:00
Riz Ashraf 9f00d8298f fix(mcp): Add namespace fields to EntityInput and RelationInput DTOs to fix test regression 2026-10-10 11:08:25 +01:00
Riz Ashraf e006ef1220 perf(mcp): Replace redundant LLM usages with deterministic logic
- Refactored memory_consolidation_worker to use purely deterministic string normalization ((N)$ regex stripping & lowercase) instead of passing graph entities to Ollama for duplication detection.
- Removed redundant Ollama architectural summarization from log_code_change tool since the agent already provides a descriptive summary.
- Removed redundant Ollama fix summarization from log_error_fix tool since the agent already provides a solution string.
2026-10-10 10:58:50 +01:00
Riz Ashraf 26eaf03b6f fix: ensure tagged-enum schema injection meets tool expectations 2026-10-10 10:35:27 +01:00
Riz Ashraf 2cfaeda0cb Refactor remaining tools to tagged enums
Refactored DecisionsTool, TechDebtTool, ManageCheckpointTool, HypothesesTool, AgentSignalsTool to use tagged enums for precise JSON schemas, and updated their handlers.
2026-10-10 10:23:58 +01:00
Riz Ashraf 8ff43487bc fix: correct tests following strict removal of legacy json aliases 2026-10-10 08:24:42 +01:00
Riz Ashraf a9ed5ae9c8 refactor: add missing schemars descriptions and correct mismatched docstrings 2026-10-10 04:33:18 +01:00
Riz Ashraf 48f49d5d02 chore: strictly remove legacy JSON field aliases from tool schemas 2026-10-10 04:20:31 +01:00
Riz Ashraf 2ae343d968 refactor: clean up redundant serde aliases in tools schema 2026-10-10 03:54:16 +01:00
Riz Ashraf 27416e4ee9 fix(nvim-core): implement missing json-rpc actions for buffer, workspace, ui, and intelligence tools 2026-10-10 03:06:14 +01:00
Reazul Ashraf 4bfb3b6359 chore: remove vertical split requirement for nvim markdown presentation 2026-10-10 02:28:10 +01:00
Riz Ashraf 36b86e5b92 chore: remove unused redb::ReadableTable import in db.rs 2026-10-09 09:43:02 +01:00
Riz Ashraf 0c2fc96ff8 chore: housekeeping 2026-10-09 09:33:54 +01:00
Riz Ashraf 4ea8f861cc refactor(nvim-core): map requests directly to mega-tools and remove legacy routing 2026-10-09 06:43:05 +01:00
19 changed files with 1166 additions and 1658 deletions

No files matched your search

Generated
+40 -4
View File
@@ -2651,7 +2651,7 @@ dependencies = [
"reqwest", "reqwest",
"rmcp", "rmcp",
"rten", "rten",
"schemars", "schemars 1.2.2",
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
@@ -3009,6 +3009,7 @@ dependencies = [
"mcp-stdio", "mcp-stdio",
"rmcp", "rmcp",
"rmpv", "rmpv",
"schemars 0.8.22",
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",
@@ -3859,7 +3860,7 @@ dependencies = [
"pastey 0.2.3", "pastey 0.2.3",
"pin-project-lite", "pin-project-lite",
"rmcp-macros", "rmcp-macros",
"schemars", "schemars 1.2.2",
"serde", "serde",
"serde_json", "serde_json",
"thiserror 2.0.20", "thiserror 2.0.20",
@@ -4179,6 +4180,18 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "schemars"
version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
dependencies = [
"dyn-clone",
"schemars_derive 0.8.22",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "schemars" name = "schemars"
version = "1.2.2" version = "1.2.2"
@@ -4188,11 +4201,23 @@ dependencies = [
"chrono", "chrono",
"dyn-clone", "dyn-clone",
"ref-cast", "ref-cast",
"schemars_derive", "schemars_derive 1.2.2",
"serde", "serde",
"serde_json", "serde_json",
] ]
[[package]]
name = "schemars_derive"
version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals 0.29.1",
"syn 2.0.119",
]
[[package]] [[package]]
name = "schemars_derive" name = "schemars_derive"
version = "1.2.2" version = "1.2.2"
@@ -4201,7 +4226,7 @@ checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde_derive_internals", "serde_derive_internals 0.30.0",
"syn 3.0.6", "syn 3.0.6",
] ]
@@ -4276,6 +4301,17 @@ dependencies = [
"syn 3.0.6", "syn 3.0.6",
] ]
[[package]]
name = "serde_derive_internals"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]] [[package]]
name = "serde_derive_internals" name = "serde_derive_internals"
version = "0.30.0" version = "0.30.0"
+2 -2
View File
@@ -34,9 +34,9 @@ Before making expensive active tool calls, read available MCP resources:
- **Error Fixes**: The moment a tricky, undocumented, or environment-specific bug is resolved, IMMEDIATELY call `log_error_fix`. Supply `repo_name`, `error_category`, and `stack_trace` so future searches can perform embedding-based match via `search_error_fixes`. - **Error Fixes**: The moment a tricky, undocumented, or environment-specific bug is resolved, IMMEDIATELY call `log_error_fix`. Supply `repo_name`, `error_category`, and `stack_trace` so future searches can perform embedding-based match via `search_error_fixes`.
- **Tech Debt**: If you notice an anti-pattern but deliberately skip fixing it to focus on a feature, IMMEDIATELY call `tech_debt` (action: "log") with `description`, `file_path`, `line_range`, `workaround`, `effort_estimate`, and `severity`. - **Tech Debt**: If you notice an anti-pattern but deliberately skip fixing it to focus on a feature, IMMEDIATELY call `tech_debt` (action: "log") with `description`, `file_path`, `line_range`, `workaround`, `effort_estimate`, and `severity`.
- **Architectural Decisions (ADR) & Lifecycle Closure**: - **Architectural Decisions (ADR) & Lifecycle Closure**:
- When selecting design patterns, crate choices, or system structure, call `decisions` (action: "log") with `author`, `affected_components`, `alternatives_considered`, `decision`, and `consequence`. - When selecting design patterns, crate choices, or system structure, call `decisions` (action: "log") with `author`, `affected_components`, `alternatives_considered`, `decision`, and `consequences`.
- **MANDATORY Definition of Done**: When code implementing an ADR is committed, you MUST IMMEDIATELY call `decisions` (action: "update", id: "ADR-XXXX", status: "implemented", git_commit: <commit_hash>, git_branch: <branch>). NEVER leave an ADR in `accepted` once the implementing code is committed. The repository also executes `scripts/git-reconcile.py` on post-commit hooks (`just install-git-hooks`) to reconcile commit references automatically. - **MANDATORY Definition of Done**: When code implementing an ADR is committed, you MUST IMMEDIATELY call `decisions` (action: "update", id: "ADR-XXXX", status: "implemented", git_commit: <commit_hash>, git_branch: <branch>). NEVER leave an ADR in `accepted` once the implementing code is committed. The repository also executes `scripts/git-reconcile.py` on post-commit hooks (`just install-git-hooks`) to reconcile commit references automatically.
- **Task Management**: When creating tasks, supply `priority` ('low'|'medium'|'high'|'urgent'), `assigned_agent` (e.g. subagent role), `verification_command` (automated test command), and `acceptance_criteria`. - **Task Management**: When creating tasks, supply `priority` ('low'|'medium'|'high'|'urgent'), `assigned_agent` (e.g. subagent role), `verification_command` (automated test command), and `criteria`.
- **VCS & SVN Agnosticism**: Supply `vcs_type` ('git'|'svn'|'hg'), `vcs_revision` (git hash or svn revision like 'r12345'), and `upstream_url` to `log_code_change` and workspace tools. - **VCS & SVN Agnosticism**: Supply `vcs_type` ('git'|'svn'|'hg'), `vcs_revision` (git hash or svn revision like 'r12345'), and `upstream_url` to `log_code_change` and workspace tools.
- **Terminal & Shell Context**: Terminal sessions and commands are automatically tracked in the server over zero-latency UDP. Query `/terminal/history` or `memory://terminal/recent` when analyzing shell execution context. - **Terminal & Shell Context**: Terminal sessions and commands are automatically tracked in the server over zero-latency UDP. Query `/terminal/history` or `memory://terminal/recent` when analyzing shell execution context.
- **Hypotheses & Root Cause Analysis**: When diagnosing complex bugs or race conditions, call `hypotheses` (action: "log" / "query") to record test evidence and maintain reasoning trails across sessions. - **Hypotheses & Root Cause Analysis**: When diagnosing complex bugs or race conditions, call `hypotheses` (action: "log" / "query") to record test evidence and maintain reasoning trails across sessions.
+1 -1
View File
@@ -26,7 +26,7 @@ Headless mode (`nvim --headless`) is strictly banned for interactive edits.
Headless instances are allowed ONLY for non-interactive background batch processing (e.g., project-wide formatting or linting). Headless instances are allowed ONLY for non-interactive background batch processing (e.g., project-wide formatting or linting).
## 4. UI Presentation & Chat Console Minimization ## 4. UI Presentation & Chat Console Minimization
Never output large plans, context blocks, or architectural discussions to the chat console if Neovim is running. You MUST use the `nvim_buffer` and `nvim_workspace` tools to open a vertical split (e.g., `Antigravity_Plan.md` scratch buffer) and present the markdown natively. Reserve the chat console strictly for brief confirmations. Never output large plans, context blocks, or architectural discussions to the chat console if Neovim is running. You MUST use the `nvim_buffer` and `nvim_workspace` tools to open the markdown natively in the editor (e.g., `Antigravity_Plan.md` scratch buffer). Reserve the chat console strictly for brief confirmations.
## 5. Visual Cues & Auto-Save ## 5. Visual Cues & Auto-Save
When manipulating buffers via MCP: When manipulating buffers via MCP:
+1
View File
@@ -16,6 +16,7 @@ rmcp = { version = "3.4.0", features = ["server"] }
mcp-stdio = { version = "1.0.0", path = "../mcp-stdio" } mcp-stdio = { version = "1.0.0", path = "../mcp-stdio" }
bytes = "1.12.1" bytes = "1.12.1"
dashmap = "6.2.1" dashmap = "6.2.1"
schemars = "0.8"
[build-dependencies] [build-dependencies]
chrono = "0.4.45" chrono = "0.4.45"
+263 -935
View File
File diff suppressed because it is too large. Load diff
+101
View File
@@ -0,0 +1,101 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum NvimBufferTool {
Read {
file: String,
start_line: Option<i64>,
end_line: Option<i64>,
},
Replace {
file: Option<String>,
start_line: Option<i64>,
end_line: Option<i64>,
replacement_content: Option<String>,
expected_content: Option<String>,
edits: Option<Vec<BufferEdit>>,
},
Save {
file: Option<String>,
},
Undo {
steps: Option<i64>,
},
Redo {
steps: Option<i64>,
},
CreateScratch {
name: Option<String>,
content: Option<String>,
},
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct BufferEdit {
pub file: String,
pub start_line: i64,
pub end_line: i64,
pub replacement_content: String,
pub expected_content: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum NvimWorkspaceTool {
ListBuffers,
ListWindows,
Focus {
win_id: i64,
},
Split {
direction: Option<String>,
file: Option<String>,
buf_id: Option<i64>,
},
Cwd {
path: Option<String>,
},
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum NvimIntelligenceTool {
Hover { file: String, line: i64, col: i64 },
Definition { file: String, line: i64, col: i64 },
References { file: String, line: i64, col: i64 },
CodeAction { file: String, line: i64, col: i64 },
Rename { file: String, line: i64, col: i64, new_name: String },
Diagnostics { file: String },
Outline { file: String },
Query { file: String, query: Option<String>, preset: Option<String> },
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum NvimUiTool {
Highlight {
buf_id: Option<i64>,
start_line: i64,
end_line: Option<i64>,
highlight_group: Option<String>,
duration_ms: Option<i64>,
},
GhostText {
line: i64,
text: String,
highlight_group: Option<String>,
},
Clear {
buf_id: Option<i64>,
},
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum NvimExecTool {
Lua { code: String },
Vimscript { code: String },
Terminal { command: String },
}
+25
View File
@@ -0,0 +1,25 @@
use schemars::schema_for;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(JsonSchema, Deserialize, Serialize)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum TasksToolEnum {
/// Create task.
Add {
/// Task title.
title: String,
/// Task description.
description: Option<String>,
},
/// Delete task.
Delete {
/// Task ID.
id: String,
}
}
fn main() {
let schema = schema_for!(TasksToolEnum);
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
}
+6
View File
@@ -0,0 +1,6 @@
use mcp_memory_server::tools::{DecisionsTool, TechDebtTool, ManageCheckpointTool, HypothesesTool, AgentSignalsTool};
use schemars::schema_for;
fn main() {
println!("{}", serde_json::to_string_pretty(&schema_for!(AgentSignalsTool)).unwrap());
}
+1 -1
View File
@@ -1,5 +1,5 @@
use crate::store::STORE_TABLE; use crate::store::STORE_TABLE;
use redb::{Database, ReadableTable}; use redb::Database;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
+167 -29
View File
@@ -163,9 +163,15 @@ impl McpTool for CreateEntitiesHandler {
let req: CreateEntitiesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: CreateEntitiesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut inserted = Vec::new(); let mut inserted = Vec::new();
state.modify_graph(|g| { state.modify_graph(|g| {
for mut entity in req.entities { for entity_input in req.entities {
if !entity.name.is_empty() { if !entity_input.name.is_empty() {
entity.entity_type = crate::models::normalize_entity_type(&entity.entity_type); let entity = crate::models::Entity {
name: entity_input.name.clone(),
entity_type: crate::models::normalize_entity_type(&entity_input.entity_type),
observations: entity_input.observations,
namespace: entity_input.namespace.unwrap_or_else(crate::models::default_namespace),
..Default::default()
};
inserted.push(entity.clone()); inserted.push(entity.clone());
g.entities.insert(entity.name.clone(), entity); g.entities.insert(entity.name.clone(), entity);
} }
@@ -187,6 +193,108 @@ impl McpTool for CreateEntitiesHandler {
} }
} }
pub struct UpdateEntitiesHandler;
#[async_trait]
impl McpTool for UpdateEntitiesHandler {
fn name(&self) -> &'static str {
"update_entities"
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<UpdateEntitiesTool>(
"update_entities",
"Update existing entities in the knowledge graph (e.g., to rename an entity or change its type).",
)
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: UpdateEntitiesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut updated_entities = Vec::new();
let mut deleted_names = Vec::new();
let mut not_found = Vec::new();
let mut conflict_names = Vec::new();
state.modify_graph(|g| {
for update in req.updates {
if !g.entities.contains_key(&update.name) {
not_found.push(update.name.clone());
continue;
}
if let Some(new_name) = &update.new_name {
if update.name != *new_name && g.entities.contains_key(new_name) {
conflict_names.push(new_name.clone());
continue;
}
}
if let Some(mut entity) = g.entities.remove(&update.name) {
let mut renamed = false;
let old_name = entity.name.clone();
if let Some(new_name) = update.new_name {
if old_name != new_name {
entity.name = new_name.clone();
renamed = true;
deleted_names.push(old_name.clone());
}
}
if let Some(new_type) = update.new_entity_type {
entity.entity_type = crate::models::normalize_entity_type(&new_type);
}
let current_name = entity.name.clone();
if renamed {
// update relations
let mut seen = std::collections::HashSet::new();
g.relations.retain_mut(|r| {
if r.from == old_name {
r.from = current_name.clone();
}
if r.to == old_name {
r.to = current_name.clone();
}
// Keep unique relations
seen.insert(r.clone())
});
}
updated_entities.push(entity.clone());
g.entities.insert(current_name, entity);
}
}
});
if !not_found.is_empty() {
return Err(crate::error::AppError::Internal(format!(
"Error: Entities not found: {}", not_found.join(", ")
)));
}
if !conflict_names.is_empty() {
return Err(crate::error::AppError::Internal(format!(
"Error: Cannot rename to existing entity names: {}", conflict_names.join(", ")
)));
}
let idx = state.get_search_index().await;
for old_name in deleted_names {
drop(idx.delete_document(&old_name));
}
if !updated_entities.is_empty() {
let _ = idx.index_entities_batch(&updated_entities).await;
}
let names: Vec<String> = updated_entities.iter().map(|e| e.name.clone()).collect();
Ok(format!("Successfully updated {} entity/entities: {}", names.len(), names.join(", ")))
}
}
pub struct CreateRelationsHandler; pub struct CreateRelationsHandler;
#[async_trait] #[async_trait]
@@ -256,10 +364,15 @@ impl McpTool for CreateRelationsHandler {
auto_created.push(node_name); auto_created.push(node_name);
} }
} }
for mut relation in req.relations { for relation_input in req.relations {
if !relation.from.is_empty() && !relation.to.is_empty() { if !relation_input.from.is_empty() && !relation_input.to.is_empty() {
relation.relation_type = let relation = crate::models::Relation {
crate::models::normalize_relation_type(&relation.relation_type); from: relation_input.from,
to: relation_input.to,
relation_type: crate::models::normalize_relation_type(&relation_input.relation_type),
namespace: relation_input.namespace.unwrap_or_else(crate::models::default_namespace),
..Default::default()
};
added_relations.push(format!( added_relations.push(format!(
"{} -[{}]-> {}", "{} -[{}]-> {}",
relation.from, relation.relation_type, relation.to relation.from, relation.relation_type, relation.to
@@ -398,30 +511,48 @@ impl McpTool for DeleteObservationsHandler {
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: DeleteObservationsTool = let req: DeleteObservationsTool =
serde_json::from_value(args).map_err(|e| e.to_string())?; serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut missing = Vec::new();
state.read_graph(|g| {
for d in &req.deletions {
if !g.entities.contains_key(&d.entity_name) {
missing.push(d.entity_name.clone());
}
}
});
if !missing.is_empty() { let mut missing_entities = Vec::new();
return Err(crate::error::AppError::Internal(format!( let mut missing_observations = Vec::new();
"Error: Entities not found: {}. Please use the search_nodes or read_graph tools to verify the exact entity names.",
missing.join(", ")
)));
}
state.modify_graph(|master| { state.modify_graph(|master| {
for d in req.deletions { for d in req.deletions {
if let Some(e) = master.entities.get_mut(&d.entity_name) { if let Some(e) = master.entities.get_mut(&d.entity_name) {
let to_rem: HashSet<_> = d.observations.into_iter().collect(); for target in d.observations {
e.observations.retain(|o| !to_rem.contains(o)); let target_clean = target.trim();
if target_clean.is_empty() { continue; }
let initial_len = e.observations.len();
e.observations.retain(|o| {
let o_clean = o.trim();
// Exact match OR substantial substring match (to forgive LLM whitespace/newline errors)
!(o_clean == target_clean || (target_clean.len() >= 10 && o_clean.contains(target_clean)))
});
if e.observations.len() == initial_len {
missing_observations.push(target);
}
}
} else {
missing_entities.push(d.entity_name);
} }
} }
}); });
if !missing_entities.is_empty() {
return Err(crate::error::AppError::Internal(format!(
"Error: Entities not found: {}. Please use search_nodes to verify the exact entity names.",
missing_entities.join(", ")
)));
}
if !missing_observations.is_empty() {
return Err(crate::error::AppError::Internal(format!(
"Error: Failed to find {} observation(s) to delete. Make sure you provide a sufficiently long exact substring match.",
missing_observations.len()
)));
}
Ok("Observations deleted".to_string()) Ok("Observations deleted".to_string())
} }
} }
@@ -443,13 +574,21 @@ impl McpTool for DeleteRelationsHandler {
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: DeleteRelationsTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: DeleteRelationsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut missing_count = 0; let mut deleted_count = 0;
let requested_count = req.relations.len();
state.modify_graph(|master| { state.modify_graph(|master| {
let to_rem: HashSet<_> = req.relations.into_iter().collect();
let initial_len = master.relations.len(); let initial_len = master.relations.len();
master.relations.retain(|r| !to_rem.contains(r)); master.relations.retain(|r| {
missing_count = to_rem.len() - (initial_len - master.relations.len()); let should_delete = req.relations.iter().any(|target| {
target.from == r.from && target.to == r.to && target.relation_type == r.relation_type
});
!should_delete
});
deleted_count = initial_len - master.relations.len();
}); });
let missing_count = requested_count.saturating_sub(deleted_count);
if missing_count > 0 { if missing_count > 0 {
return Err(crate::error::AppError::Internal(format!( return Err(crate::error::AppError::Internal(format!(
"Error: {} relation(s) not found in graph. Please verify exact relation properties (from, to, relation_type) using read_graph or get_subgraph.", "Error: {} relation(s) not found in graph. Please verify exact relation properties (from, to, relation_type) using read_graph or get_subgraph.",
@@ -1369,10 +1508,9 @@ mod tests {
.unwrap(); .unwrap();
assert!(res.contains("Successfully created 1 relation(s)")); assert!(res.contains("Successfully created 1 relation(s)"));
// Test serde field aliases (source/target/relationType mapped to from/to/relation_type)
let alias_args = json!({ let alias_args = json!({
"relations": [ "relations": [
{"source": "A", "target": "B", "relationType": "knows"} {"from": "A", "to": "B", "relation_type": "knows"}
] ]
}); });
let alias_res = handler.execute(alias_args, state.clone()).await.unwrap(); let alias_res = handler.execute(alias_args, state.clone()).await.unwrap();
+98 -172
View File
@@ -24,24 +24,7 @@ impl McpTool for LogErrorFixHandler {
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> { 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 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 text_to_embed = format!("Signature: {}\nSolution: {}", req.signature, req.solution);
let mut solution = req.solution; let 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, 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) let embedding = crate::embedding::generate_embedding_async(text_to_embed)
.await .await
@@ -204,18 +187,6 @@ impl McpTool for LogCodeChangeHandler {
description = format!("{} [Symbols: {}]", description, symbols.join(", ")); description = format!("{} [Symbols: {}]", description, symbols.join(", "));
} }
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, None).await {
let clean = summary.trim();
if !clean.is_empty() {
description = format!("{} (AI Summary: {})", description, clean);
}
}
}
let change_kind = match req let change_kind = match req
.change_kind .change_kind
.as_deref() .as_deref()
@@ -383,18 +354,14 @@ impl McpTool for DecisionsHandler {
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: DecisionsTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: DecisionsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let ns = req match req {
.namespace DecisionsTool::Log { title, status, context, decision, consequences, namespace, repo_name, author, affected_components, alternatives_considered, supersedes, git_commit, git_branch, task_id } => {
.unwrap_or_else(crate::models::default_namespace); let ns = namespace.clone().unwrap_or_else(crate::models::default_namespace);
match req.action { let title = title;
DecisionAction::Log => { let status = status.unwrap_or_else(|| "accepted".to_string());
let title = req.title.ok_or_else(|| { let context = context.unwrap_or_default();
crate::error::AppError::Internal("Missing required parameter 'title' for action 'log'. Next step: Provide ADR 'title' string in request and retry.".to_string()) let decision = decision.unwrap_or_default();
})?; let consequence = consequences.unwrap_or_default();
let status = req.status.unwrap_or_else(|| "accepted".to_string());
let context = req.context.unwrap_or_default();
let decision = req.decision.unwrap_or_default();
let consequence = req.consequences.unwrap_or_default();
let status_lower = status.to_ascii_lowercase(); let status_lower = status.to_ascii_lowercase();
let resolved_at = if status_lower == "implemented" || status_lower == "resolved" { let resolved_at = if status_lower == "implemented" || status_lower == "resolved" {
Some(crate::handlers::utils::now_secs()) Some(crate::handlers::utils::now_secs())
@@ -407,7 +374,7 @@ impl McpTool for DecisionsHandler {
let mut adrs_to_index = Vec::new(); let mut adrs_to_index = Vec::new();
state.code.adrs.modify(|adrs| { state.code.adrs.modify(|adrs| {
if let Some(superseded_id) = &req.supersedes { if let Some(superseded_id) = &supersedes {
for old_adr in adrs.iter_mut() { for old_adr in adrs.iter_mut() {
if old_adr.id.eq_ignore_ascii_case(superseded_id) { if old_adr.id.eq_ignore_ascii_case(superseded_id) {
old_adr.status = "superseded".to_string(); old_adr.status = "superseded".to_string();
@@ -425,17 +392,17 @@ impl McpTool for DecisionsHandler {
decision: decision.clone(), decision: decision.clone(),
consequence, consequence,
status, status,
supersedes: req.supersedes, supersedes: supersedes,
timestamp: crate::handlers::utils::now_secs(), timestamp: crate::handlers::utils::now_secs(),
namespace: ns, namespace: ns,
repo_name: req.repo_name, repo_name: repo_name,
alternatives_considered: req.alternatives_considered.unwrap_or_default(), alternatives_considered: alternatives_considered.unwrap_or_default(),
affected_components: req.affected_components.unwrap_or_default(), affected_components: affected_components.unwrap_or_default(),
author: req.author, author: author,
git_commit: req.git_commit, git_commit: git_commit,
git_branch: req.git_branch, git_branch: git_branch,
resolved_at, resolved_at,
task_id: req.task_id, task_id: task_id,
}; };
adrs_to_index.push(a.clone()); adrs_to_index.push(a.clone());
@@ -453,17 +420,15 @@ impl McpTool for DecisionsHandler {
); );
Ok(format!("Logged decision {}: {}", final_id, title)) Ok(format!("Logged decision {}: {}", final_id, title))
} }
DecisionAction::Update => { DecisionsTool::Update { id, title, status, context, decision, consequences, git_commit, git_branch, task_id, namespace: _, repo_name, author, affected_components, alternatives_considered, supersedes } => {
let id = req.id.ok_or_else(|| { let id = id;
crate::error::AppError::Internal("Missing required parameter 'id' for action 'update'. Next step: Provide ADR 'id' string in request and retry.".to_string())
})?;
let mut updated_adr = None; let mut updated_adr = None;
let mut adrs_to_index = Vec::new(); let mut adrs_to_index = Vec::new();
state.code.adrs.modify(|adrs| { state.code.adrs.modify(|adrs| {
let target_pos = adrs.iter().position(|a| a.id.eq_ignore_ascii_case(&id)); let target_pos = adrs.iter().position(|a| a.id.eq_ignore_ascii_case(&id));
if let Some(pos) = target_pos { if let Some(pos) = target_pos {
if let Some(superseded_id) = &req.supersedes if let Some(superseded_id) = &supersedes
&& let Some(s_pos) = adrs && let Some(s_pos) = adrs
.iter() .iter()
.position(|a| a.id.eq_ignore_ascii_case(superseded_id)) .position(|a| a.id.eq_ignore_ascii_case(superseded_id))
@@ -474,19 +439,19 @@ impl McpTool for DecisionsHandler {
} }
let a = &mut adrs[pos]; let a = &mut adrs[pos];
if let Some(t) = req.title { if let Some(t) = title {
a.title = t; a.title = t;
} }
if let Some(c) = req.context { if let Some(c) = context {
a.context = c; a.context = c;
} }
if let Some(d) = req.decision { if let Some(d) = decision {
a.decision = d; a.decision = d;
} }
if let Some(cons) = req.consequences { if let Some(cons) = consequences {
a.consequence = cons; a.consequence = cons;
} }
if let Some(s) = req.status { if let Some(s) = status {
let s_lower = s.to_ascii_lowercase(); let s_lower = s.to_ascii_lowercase();
if (s_lower == "implemented" || s_lower == "resolved") if (s_lower == "implemented" || s_lower == "resolved")
&& a.resolved_at.is_none() && a.resolved_at.is_none()
@@ -497,29 +462,29 @@ impl McpTool for DecisionsHandler {
} }
a.status = s; a.status = s;
} }
if req.supersedes.is_some() { if supersedes.is_some() {
a.supersedes = req.supersedes; a.supersedes = supersedes;
} }
if req.repo_name.is_some() { if repo_name.is_some() {
a.repo_name = req.repo_name; a.repo_name = repo_name;
} }
if let Some(alts) = req.alternatives_considered { if let Some(alts) = alternatives_considered {
a.alternatives_considered = alts; a.alternatives_considered = alts;
} }
if let Some(aff) = req.affected_components { if let Some(aff) = affected_components {
a.affected_components = aff; a.affected_components = aff;
} }
if req.author.is_some() { if author.is_some() {
a.author = req.author; a.author = author;
} }
if req.git_commit.is_some() { if git_commit.is_some() {
a.git_commit = req.git_commit; a.git_commit = git_commit;
} }
if req.git_branch.is_some() { if git_branch.is_some() {
a.git_branch = req.git_branch; a.git_branch = git_branch;
} }
if req.task_id.is_some() { if task_id.is_some() {
a.task_id = req.task_id; a.task_id = task_id;
} }
adrs_to_index.push(a.clone()); adrs_to_index.push(a.clone());
@@ -548,9 +513,10 @@ impl McpTool for DecisionsHandler {
))) )))
} }
} }
DecisionAction::Query => { DecisionsTool::Query { query, namespace, limit, include_body } => {
let limit = req.limit.unwrap_or(20); let ns = namespace.clone().unwrap_or_else(crate::models::default_namespace);
let include_body = req.include_body.unwrap_or(true); let limit = limit.unwrap_or(20);
let include_body = include_body.unwrap_or(true);
let data = state.code.adrs.read_with(|adrs| { let data = state.code.adrs.read_with(|adrs| {
let filtered: Vec<_> = adrs let filtered: Vec<_> = adrs
.iter() .iter()
@@ -558,7 +524,7 @@ impl McpTool for DecisionsHandler {
if !a.namespace.eq_ignore_ascii_case(&ns) && ns != "global" { if !a.namespace.eq_ignore_ascii_case(&ns) && ns != "global" {
return false; return false;
} }
if let Some(q) = &req.query { if let Some(q) = &query {
crate::handlers::utils::contains_ignore_ascii_case(&a.title, q) crate::handlers::utils::contains_ignore_ascii_case(&a.title, q)
|| crate::handlers::utils::contains_ignore_ascii_case( || crate::handlers::utils::contains_ignore_ascii_case(
&a.context, q, &a.context, q,
@@ -601,10 +567,8 @@ impl McpTool for DecisionsHandler {
})?; })?;
Ok(data) Ok(data)
} }
DecisionAction::Delete => { DecisionsTool::Delete { id } => {
let id = req.id.ok_or_else(|| { let id = id;
crate::error::AppError::Internal("Missing required parameter 'id' for action 'delete'. Next step: Provide ADR 'id' string in request and retry.".to_string())
})?;
let mut found = false; let mut found = false;
state.code.adrs.modify(|adrs| { state.code.adrs.modify(|adrs| {
if let Some(pos) = adrs.iter().position(|a| a.id == id) { if let Some(pos) = adrs.iter().position(|a| a.id == id) {
@@ -644,15 +608,11 @@ impl McpTool for TechDebtHandler {
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: TechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: TechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let ns = req match req {
.namespace TechDebtTool::Log { description, title: _, ideal_solution, git_commit, git_branch, symbol_references: _, line_range, namespace, severity, repo_name, file_path, workaround, effort_estimate } => {
.unwrap_or_else(crate::models::default_namespace); let ns = namespace.clone().unwrap_or_else(crate::models::default_namespace);
match req.action { let desc = description;
TechDebtAction::Log => { let ideal = ideal_solution.unwrap_or_default();
let desc = req.description.or(req.title).ok_or_else(|| {
crate::error::AppError::Internal("Missing required parameter 'description' for action 'log'. Next step: Provide tech debt 'description' in request and retry.".to_string())
})?;
let ideal = req.ideal_solution.unwrap_or_default();
let text_to_embed = format!("Description: {}\nIdeal Solution: {}", desc, ideal); let text_to_embed = format!("Description: {}\nIdeal Solution: {}", desc, ideal);
let embedding = crate::embedding::generate_embedding_async(text_to_embed) let embedding = crate::embedding::generate_embedding_async(text_to_embed)
.await .await
@@ -665,15 +625,15 @@ impl McpTool for TechDebtHandler {
ideal_solution: ideal, ideal_solution: ideal,
is_resolved: false, is_resolved: false,
created_at: crate::handlers::utils::now_secs(), created_at: crate::handlers::utils::now_secs(),
git_commit: req.git_commit, git_commit: git_commit,
git_branch: req.git_branch, git_branch: git_branch,
embedding, embedding,
repo_name: req.repo_name, repo_name: repo_name,
severity: req.severity, severity: severity,
file_path: req.file_path, file_path: file_path,
line_range: req.line_range, line_range: line_range,
workaround: req.workaround, workaround: workaround,
effort_estimate: req.effort_estimate, effort_estimate: effort_estimate,
}); });
if debts.len() > 300 { if debts.len() > 300 {
let severity_rank = let severity_rank =
@@ -697,10 +657,8 @@ impl McpTool for TechDebtHandler {
}); });
Ok("Tech debt logged".to_string()) Ok("Tech debt logged".to_string())
} }
TechDebtAction::Resolve => { TechDebtTool::Resolve { id } => {
let id = req.id.ok_or_else(|| { let id = id;
crate::error::AppError::Internal("Missing required parameter 'id' for action 'resolve'. Next step: Provide tech debt 'id' string in request and retry.".to_string())
})?;
let mut found = false; let mut found = false;
state.code.tech_debts.modify(|debts| { state.code.tech_debts.modify(|debts| {
for d in debts.iter_mut() { for d in debts.iter_mut() {
@@ -720,9 +678,10 @@ impl McpTool for TechDebtHandler {
)) ))
} }
} }
TechDebtAction::List => { TechDebtTool::List { include_resolved, namespace, summary_level, max_tokens } => {
let inc = req.include_resolved.unwrap_or(false); let ns = namespace.clone().unwrap_or_else(crate::models::default_namespace);
let level = req.summary_level.as_deref().unwrap_or("detailed"); let inc = include_resolved.unwrap_or(false);
let level = summary_level.as_deref().unwrap_or("detailed");
let data = state.code.tech_debts.read_with(|debts| { let data = state.code.tech_debts.read_with(|debts| {
let filtered: Vec<_> = debts let filtered: Vec<_> = debts
.iter() .iter()
@@ -743,7 +702,7 @@ impl McpTool for TechDebtHandler {
}) })
.collect(); .collect();
let mut json_str = serde_yaml::to_string(&filtered)?; let mut json_str = serde_yaml::to_string(&filtered)?;
if let Some(max_t) = req.max_tokens { if let Some(max_t) = max_tokens {
let char_limit = max_t * 4; let char_limit = max_t * 4;
if json_str.len() > char_limit { if json_str.len() > char_limit {
json_str.truncate(char_limit); json_str.truncate(char_limit);
@@ -1199,13 +1158,9 @@ impl McpTool for ManageCheckpointHandler {
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: ManageCheckpointTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: ManageCheckpointTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
match req.action { match req {
CheckpointAction::Create => { ManageCheckpointTool::Create { name_or_id, description, namespace } => {
let name = req.name_or_id.ok_or_else(|| { let name = name_or_id.ok_or_else(|| crate::error::AppError::Internal("name_or_id is required".to_string()))?;
crate::error::AppError::Internal(
"name_or_id is required for 'create' action".to_string(),
)
})?;
let target_dir = state.base_dir.join("checkpoints").join(&name); let target_dir = state.base_dir.join("checkpoints").join(&name);
if let Err(e) = std::fs::create_dir_all(&target_dir) { if let Err(e) = std::fs::create_dir_all(&target_dir) {
@@ -1230,15 +1185,12 @@ impl McpTool for ManageCheckpointHandler {
.read_with(|d| serde_json::to_string(d).unwrap_or_default()); .read_with(|d| serde_json::to_string(d).unwrap_or_default());
let _ = std::fs::write(target_dir.join("tech_debts.json"), debts_json); let _ = std::fs::write(target_dir.join("tech_debts.json"), debts_json);
if let Some(desc) = &req.description { if let Some(desc) = &description {
let snapshot_id = format!( let snapshot_id = format!(
"SNAP-{}", "SNAP-{}",
uuid::Uuid::new_v4().to_string()[..8].to_uppercase() uuid::Uuid::new_v4().to_string()[..8].to_uppercase()
); );
let ns = req let ns = namespace.clone().unwrap_or_else(|| "global".to_string());
.namespace
.clone()
.unwrap_or_else(|| "global".to_string());
let snapshot = crate::models::StateSnapshot { let snapshot = crate::models::StateSnapshot {
id: snapshot_id, id: snapshot_id,
timestamp: crate::handlers::utils::now_secs(), timestamp: crate::handlers::utils::now_secs(),
@@ -1251,12 +1203,8 @@ impl McpTool for ManageCheckpointHandler {
Ok(format!("Checkpoint '{}' created successfully.", name)) Ok(format!("Checkpoint '{}' created successfully.", name))
} }
CheckpointAction::Restore => { ManageCheckpointTool::Restore { name_or_id, namespace: _ } => {
let name = req.name_or_id.ok_or_else(|| { let name = name_or_id.ok_or_else(|| crate::error::AppError::Internal("name_or_id is required".to_string()))?;
crate::error::AppError::Internal(
"name_or_id is required for 'restore' action".to_string(),
)
})?;
let target_dir = state.base_dir.join("checkpoints").join(&name); let target_dir = state.base_dir.join("checkpoints").join(&name);
if !target_dir.exists() { if !target_dir.exists() {
@@ -1297,7 +1245,7 @@ impl McpTool for ManageCheckpointHandler {
Ok(format!("Checkpoint '{}' restored successfully.", name)) Ok(format!("Checkpoint '{}' restored successfully.", name))
} }
CheckpointAction::List => { ManageCheckpointTool::List { namespace: _ } => {
let mut list = Vec::new(); let mut list = Vec::new();
let checkpoints_dir = state.base_dir.join("checkpoints"); let checkpoints_dir = state.base_dir.join("checkpoints");
if let Ok(entries) = std::fs::read_dir(&checkpoints_dir) { if let Ok(entries) = std::fs::read_dir(&checkpoints_dir) {
@@ -1315,12 +1263,8 @@ impl McpTool for ManageCheckpointHandler {
} }
Ok(serde_yaml::to_string(&list)?) Ok(serde_yaml::to_string(&list)?)
} }
CheckpointAction::Delete => { ManageCheckpointTool::Delete { name_or_id, namespace: _ } => {
let name = req.name_or_id.ok_or_else(|| { let name = name_or_id.ok_or_else(|| crate::error::AppError::Internal("name_or_id is required".to_string()))?;
crate::error::AppError::Internal(
"name_or_id is required for 'delete' action".to_string(),
)
})?;
let target_dir = state.base_dir.join("checkpoints").join(&name); let target_dir = state.base_dir.join("checkpoints").join(&name);
if target_dir.exists() { if target_dir.exists() {
let _ = std::fs::remove_dir_all(&target_dir); let _ = std::fs::remove_dir_all(&target_dir);
@@ -1516,14 +1460,9 @@ impl McpTool for HypothesesHandler {
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: HypothesesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: HypothesesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
match req {
match req.action { HypothesesTool::Log { hypothesis, task_id, status, evidence } => {
HypothesisAction::Log => { let hyp_text = hypothesis;
let hyp_text = req.hypothesis.ok_or_else(|| {
crate::error::AppError::Internal(
"Missing required 'hypothesis' for action 'log'".to_string(),
)
})?;
let hyp_id = format!( let hyp_id = format!(
"HYP-{}", "HYP-{}",
uuid::Uuid::new_v4().to_string()[..8].to_uppercase() uuid::Uuid::new_v4().to_string()[..8].to_uppercase()
@@ -1532,10 +1471,10 @@ impl McpTool for HypothesesHandler {
let record = crate::models::Hypothesis { let record = crate::models::Hypothesis {
id: hyp_id.clone(), id: hyp_id.clone(),
task_id: req.task_id, task_id: task_id,
hypothesis: hyp_text, hypothesis: hyp_text,
status: req.status.unwrap_or_else(|| "unverified".to_string()), status: status.unwrap_or_else(|| "unverified".to_string()),
evidence: req.evidence, evidence: evidence,
timestamp, timestamp,
..Default::default() ..Default::default()
}; };
@@ -1544,17 +1483,17 @@ impl McpTool for HypothesesHandler {
Ok(format!("Hypothesis '{}' logged successfully.", hyp_id)) Ok(format!("Hypothesis '{}' logged successfully.", hyp_id))
} }
HypothesisAction::Query => { HypothesesTool::Query { query, task_id, status: _ } => {
let hypotheses = state.code.hypotheses.read_with(|h| h.clone()); let hypotheses = state.code.hypotheses.read_with(|h| h.clone());
let filtered: Vec<_> = hypotheses let filtered: Vec<_> = hypotheses
.into_iter() .into_iter()
.filter(|h| { .filter(|h| {
if let Some(tid) = &req.task_id if let Some(tid) = &task_id
&& h.task_id.as_ref() != Some(tid) && h.task_id.as_ref() != Some(tid)
{ {
return false; return false;
} }
if let Some(q) = &req.query { if let Some(q) = &query {
let lq = q.to_lowercase(); let lq = q.to_lowercase();
return h.hypothesis.to_lowercase().contains(&lq) return h.hypothesis.to_lowercase().contains(&lq)
|| h.evidence || h.evidence
@@ -1665,24 +1604,11 @@ impl McpTool for AgentSignalsHandler {
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: AgentSignalsTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: AgentSignalsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
match req {
match req.action { AgentSignalsTool::Broadcast { sender, signal_type, payload, ttl_seconds } => {
AgentSignalAction::Broadcast => { let sender = sender;
let sender = req.sender.ok_or_else(|| { let signal_type = signal_type;
crate::error::AppError::Internal( let payload = payload;
"Missing required 'sender' for action 'broadcast'".to_string(),
)
})?;
let signal_type = req.signal_type.ok_or_else(|| {
crate::error::AppError::Internal(
"Missing required 'signal_type' for action 'broadcast'".to_string(),
)
})?;
let payload = req.payload.ok_or_else(|| {
crate::error::AppError::Internal(
"Missing required 'payload' for action 'broadcast'".to_string(),
)
})?;
let timestamp = std::time::SystemTime::now() let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
@@ -1696,7 +1622,7 @@ impl McpTool for AgentSignalsHandler {
signal_type: signal_type.clone(), signal_type: signal_type.clone(),
payload, payload,
timestamp, timestamp,
ttl_seconds: req.ttl_seconds, ttl_seconds: ttl_seconds,
..Default::default() ..Default::default()
}; };
@@ -1724,7 +1650,7 @@ impl McpTool for AgentSignalsHandler {
sig_id, sender sig_id, sender
)) ))
} }
AgentSignalAction::Query => { AgentSignalsTool::Query { sender, signal_type, limit } => {
let now = std::time::SystemTime::now() let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default() .unwrap_or_default()
@@ -1739,19 +1665,19 @@ impl McpTool for AgentSignalsHandler {
{ {
return false; return false;
} }
if let Some(sender) = &req.sender if let Some(sender) = &sender
&& s.sender.to_lowercase() != sender.to_lowercase() && s.sender.to_lowercase() != sender.to_lowercase()
{ {
return false; return false;
} }
if let Some(st) = &req.signal_type if let Some(st) = &signal_type
&& s.signal_type.to_lowercase() != st.to_lowercase() && s.signal_type.to_lowercase() != st.to_lowercase()
{ {
return false; return false;
} }
true true
}) })
.take(req.limit.unwrap_or(20)) .take(limit.unwrap_or(20))
.cloned() .cloned()
.collect::<Vec<_>>() .collect::<Vec<_>>()
}); });
@@ -1900,7 +1826,7 @@ mod tests {
"title": "Architecture", "title": "Architecture",
"context": "Needs DB", "context": "Needs DB",
"decision": "Use SQLite", "decision": "Use SQLite",
"consequence": "Simple", "consequences": "Simple",
}); });
let res1 = decision_handler let res1 = decision_handler
.execute(args_dec, state.clone()) .execute(args_dec, state.clone())
@@ -2112,7 +2038,7 @@ mod tests {
"title": "Use Axum", "title": "Use Axum",
"context": "Architecture choice", "context": "Architecture choice",
"decision": "Adopt Axum for web framework", "decision": "Adopt Axum for web framework",
"consequence": "Fast async API routing" "consequences": "Fast async API routing"
}), }),
state.clone(), state.clone(),
) )
+35 -68
View File
@@ -23,15 +23,12 @@ impl McpTool for TasksHandler {
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: TasksTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: TasksTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
match req.action { match req {
TaskAction::Add => { TasksTool::Add { title, description, parent_id, dependencies, git_branch, namespace, repo_name, priority, assigned_agent, verification_command } => {
let title = req.title.ok_or_else(|| { let description = description.unwrap_or_default();
crate::error::AppError::Internal("Missing required parameter 'title' for action 'add'. Next step: Provide non-empty 'title' string in request and retry.".to_string())
})?;
let description = req.description.unwrap_or_default();
let now = crate::handlers::utils::now_secs(); let now = crate::handlers::utils::now_secs();
let task_id = uuid::Uuid::new_v4().to_string(); let task_id = uuid::Uuid::new_v4().to_string();
let deps = req.dependencies.unwrap_or_default(); let deps = dependencies.unwrap_or_default();
let task = Task { let task = Task {
id: task_id.clone(), id: task_id.clone(),
@@ -40,16 +37,16 @@ impl McpTool for TasksHandler {
description, description,
created_at: now, created_at: now,
updated_at: now, updated_at: now,
git_branch: req.git_branch, git_branch,
parent_id: req.parent_id, parent_id,
dependencies: deps, dependencies: deps,
acceptance_criteria: vec![], acceptance_criteria: vec![],
expires_at: None, expires_at: None,
namespace: req.namespace.unwrap_or_else(|| "default".to_string()), namespace: namespace.unwrap_or_else(|| "default".to_string()),
repo_name: req.repo_name, repo_name,
priority: req.priority, priority,
assigned_agent: req.assigned_agent, assigned_agent,
verification_command: req.verification_command, verification_command,
}; };
let idx = state.get_search_index().await; let idx = state.get_search_index().await;
drop(idx.index_task(&task)); drop(idx.index_task(&task));
@@ -75,13 +72,7 @@ impl McpTool for TasksHandler {
}); });
Ok(format!("Task added with ID: {}", task_id)) Ok(format!("Task added with ID: {}", task_id))
} }
TaskAction::Update => { TasksTool::Update { id, status } => {
let id = req.id.ok_or_else(|| {
crate::error::AppError::Internal("Missing required parameter 'id' for action 'update'. Next step: Provide task 'id' string in request and retry.".to_string())
})?;
let status = req.status.ok_or_else(|| {
crate::error::AppError::Internal("Missing required parameter 'status' for action 'update'. Next step: Provide valid 'status' ('pending', 'active', 'in_progress', 'blocked', 'completed', or 'cancelled') in request and retry.".to_string())
})?;
let target_status = status.to_lowercase(); let target_status = status.to_lowercase();
let mut found = false; let mut found = false;
let mut blocked = false; let mut blocked = false;
@@ -202,10 +193,7 @@ impl McpTool for TasksHandler {
)) ))
} }
} }
TaskAction::Delete => { TasksTool::Delete { id } => {
let id = req.id.ok_or_else(|| {
crate::error::AppError::Internal("Missing required parameter 'id' for action 'delete'. Next step: Provide task 'id' string in request and retry.".to_string())
})?;
let mut deleted_count = 0; let mut deleted_count = 0;
let mut actually_deleted = Vec::new(); let mut actually_deleted = Vec::new();
state.project.tasks.modify(|tasks| { state.project.tasks.modify(|tasks| {
@@ -267,19 +255,19 @@ impl McpTool for TasksHandler {
)) ))
} }
} }
TaskAction::List => { TasksTool::List { git_branch, status, summary_level, max_tokens, offset } => {
let level = req.summary_level.as_deref().unwrap_or("detailed"); let level = summary_level.as_deref().unwrap_or("detailed");
let offset = req.offset.unwrap_or(0); let offset = offset.unwrap_or(0);
let data = state.project.tasks.read_with(|tasks| { let data = state.project.tasks.read_with(|tasks| {
let matching: Vec<_> = tasks let matching: Vec<_> = tasks
.iter() .iter()
.filter(|t| { .filter(|t| {
let status_match = match req.status.as_deref() { let status_match = match status.as_deref() {
Some("all") => true, Some("all") => true,
Some(s) => t.status.eq_ignore_ascii_case(s), Some(s) => t.status.eq_ignore_ascii_case(s),
None => t.is_active(), None => t.is_active(),
}; };
let branch_match = match &req.git_branch { let branch_match = match &git_branch {
Some(branch) => t.git_branch.is_none() || t.git_branch.as_deref() == Some(branch.as_str()), Some(branch) => t.git_branch.is_none() || t.git_branch.as_deref() == Some(branch.as_str()),
None => true, None => true,
}; };
@@ -297,7 +285,7 @@ impl McpTool for TasksHandler {
}) })
.collect(); .collect();
let mut json_str = serde_yaml::to_string(&filtered)?; let mut json_str = serde_yaml::to_string(&filtered)?;
if let Some(max_t) = req.max_tokens { if let Some(max_t) = max_tokens {
let char_limit = max_t * 4; let char_limit = max_t * 4;
while json_str.len() > char_limit && !filtered.is_empty() { while json_str.len() > char_limit && !filtered.is_empty() {
filtered.pop(); filtered.pop();
@@ -308,13 +296,7 @@ impl McpTool for TasksHandler {
})?; })?;
Ok(data) Ok(data)
} }
TaskAction::SetCriteria => { TasksTool::SetCriteria { id, criteria: criteria_list } => {
let id = req.id.or(req.title.clone()).ok_or_else(|| {
crate::error::AppError::Internal("Missing required parameter 'id' or 'title' for action 'set_criteria'. Next step: Provide task 'id' string in request and retry.".to_string())
})?;
let criteria_list = req.criteria.ok_or_else(|| {
crate::error::AppError::Internal("Missing required parameter 'criteria' for action 'set_criteria'. Next step: Provide array of acceptance criteria strings in request and retry.".to_string())
})?;
let mut success = false; let mut success = false;
let mut updated_task: Option<Task> = None; let mut updated_task: Option<Task> = None;
state.project.tasks.modify(|tasks| { state.project.tasks.modify(|tasks| {
@@ -355,11 +337,8 @@ impl McpTool for TasksHandler {
)) ))
} }
} }
TaskAction::Verify => { TasksTool::Verify { id, proof } => {
let id = req.id.ok_or_else(|| { let proof_str = proof.unwrap_or_else(|| "Verified".to_string());
crate::error::AppError::Internal("Missing required parameter 'id' for action 'verify'. Next step: Provide task 'id' string in request and retry.".to_string())
})?;
let proof_str = req.proof.unwrap_or_else(|| "Verified".to_string());
let mut success = false; let mut success = false;
let mut already_met = false; let mut already_met = false;
let mut updated_task: Option<Task> = None; let mut updated_task: Option<Task> = None;
@@ -436,41 +415,29 @@ impl McpTool for MilestonesHandler {
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: MilestonesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: MilestonesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
match req.action { match req {
MilestoneAction::Add => { MilestonesTool::Add { title, namespace, target_date, description, deliverables, task_ids, repo_name } => {
let title = req.title.ok_or_else(|| { let ns = namespace.unwrap_or_else(crate::models::default_namespace);
crate::error::AppError::Internal("Missing required parameter 'title' for action 'add'. Next step: Provide non-empty 'title' string in request and retry.".to_string())
})?;
let ns = req
.namespace
.unwrap_or_else(crate::models::default_namespace);
state.project.milestones.modify(|ms| { state.project.milestones.modify(|ms| {
ms.push(crate::models::Milestone { ms.push(crate::models::Milestone {
id: uuid::Uuid::new_v4().to_string(), id: uuid::Uuid::new_v4().to_string(),
title, title,
status: "pending".to_string(), status: "pending".to_string(),
namespace: ns, namespace: ns,
target_date: req target_date: target_date
.target_date
.as_deref() .as_deref()
.and_then(|s| s.parse::<u64>().ok()), .and_then(|s| s.parse::<u64>().ok()),
description: req.description, description: description,
task_ids: req.task_ids.unwrap_or_default(), task_ids: task_ids.unwrap_or_default(),
deliverables: req.deliverables.unwrap_or_default(), deliverables: deliverables.unwrap_or_default(),
repo_name: req.repo_name, repo_name,
..Default::default() ..Default::default()
}) })
}); });
crate::handlers::reconciliation::recalculate_milestones(&state); crate::handlers::reconciliation::recalculate_milestones(&state);
Ok("Milestone added".to_string()) Ok("Milestone added".to_string())
} }
MilestoneAction::Update => { MilestonesTool::Update { id, status, deliverables, task_ids } => {
let id = req.id.ok_or_else(|| {
crate::error::AppError::Internal("Missing required parameter 'id' for action 'update'. Next step: Provide milestone 'id' string in request and retry.".to_string())
})?;
let status = req.status.ok_or_else(|| {
crate::error::AppError::Internal("Missing required parameter 'status' for action 'update'. Next step: Provide milestone 'status' in request and retry.".to_string())
})?;
let mut found = false; let mut found = false;
state.project.milestones.modify(|ms| { state.project.milestones.modify(|ms| {
for m in ms.iter_mut() { for m in ms.iter_mut() {
@@ -482,9 +449,9 @@ impl McpTool for MilestonesHandler {
{ {
m.completed_at = Some(crate::handlers::utils::now_secs()); m.completed_at = Some(crate::handlers::utils::now_secs());
} }
if let Some(task_ids) = &req.task_ids { if let Some(task_ids) = &task_ids {
m.task_ids = task_ids.clone(); m.task_ids = task_ids.clone();
if let Some(deliverables) = &req.deliverables { if let Some(deliverables) = &deliverables {
m.deliverables = deliverables.clone(); m.deliverables = deliverables.clone();
} }
} }
@@ -502,12 +469,12 @@ impl McpTool for MilestonesHandler {
)) ))
} }
} }
MilestoneAction::List => { MilestonesTool::List { namespace } => {
let data = state.project.milestones.read_with(|items| { let data = state.project.milestones.read_with(|items| {
let filtered: Vec<_> = items let filtered: Vec<_> = items
.iter() .iter()
.filter(|i| { .filter(|i| {
if let Some(ns) = &req.namespace { if let Some(ns) = &namespace {
&i.namespace == ns &i.namespace == ns
} else { } else {
true true
+44 -59
View File
@@ -285,76 +285,61 @@ pub async fn memory_consolidation_worker(state: Arc<MemoryState>) {
_ = interval.tick() => {}, _ = interval.tick() => {},
} }
let entities: Vec<_> = state.graph.read_with(|g| { let entities: Vec<String> = state.graph.read_with(|g| {
g.entities g.entities.keys().cloned().collect()
.values()
.map(|e| (e.name.clone(), e.entity_type.clone()))
.collect()
}); });
if entities.len() < 2 { if entities.len() < 2 {
continue; continue;
} }
let mut entity_summaries = String::new(); let mut duplicates = None;
for (name, e_type) in entities.iter().take(50) { let mut seen = std::collections::HashMap::new();
entity_summaries.push_str(&format!("- [{}] {}\n", e_type, name)); for name in &entities {
} let normalized: String = name
.chars()
.filter(|c| c.is_alphanumeric())
.flat_map(|c| c.to_lowercase())
.collect();
let prompt = format!( // Skip empty normalized names
"Analyze the following list of entities and identify exactly TWO that represent the exact same concept or item but have slightly different names (e.g. 'auth_service' and 'AuthService'). Return ONLY a valid JSON array containing exactly two strings: the two names to merge. If no obvious duplicates exist, return an empty array []. Do not output any markdown formatting or extra text.\n\nEntities:\n{}", if normalized.is_empty() {
entity_summaries continue;
); }
if let Ok(response) = state if let Some(existing) = seen.insert(normalized, name.clone()) {
.ollama if existing != *name {
.generate( duplicates = Some((existing, name.clone()));
&prompt, break;
None,
Some("You are a helpful JSON-only data deduplication assistant. Output only JSON."),
Some("json"),
)
.await
{
let cleaned = response
.trim()
.trim_start_matches("```json")
.trim_start_matches("```")
.trim_end_matches("```")
.trim();
if let Ok(duplicates) = serde_json::from_str::<Vec<String>>(cleaned)
&& duplicates.len() == 2
{
let e1_name = &duplicates[0];
let e2_name = &duplicates[1];
if e1_name != e2_name {
tracing::info!(
"Memory Consolidation Daemon: Merging '{}' into '{}'",
e2_name,
e1_name
);
state.modify_graph(|g| {
if let Some(mut e2) = g.entities.remove(e2_name) {
if let Some(e1) = g.entities.get_mut(e1_name) {
e1.observations.append(&mut e2.observations);
} else {
g.entities.insert(e2_name.clone(), e2);
}
}
for rel in g.relations.iter_mut() {
if rel.from == *e2_name {
rel.from = e1_name.clone();
}
if rel.to == *e2_name {
rel.to = e1_name.clone();
}
}
});
} }
} }
} }
if let Some((e1_name, e2_name)) = duplicates {
tracing::info!(
"Memory Consolidation Daemon: Merging '{}' into '{}'",
e2_name,
e1_name
);
state.modify_graph(|g| {
if let Some(mut e2) = g.entities.remove(&e2_name) {
if let Some(e1) = g.entities.get_mut(&e1_name) {
e1.observations.append(&mut e2.observations);
} else {
g.entities.insert(e2_name.clone(), e2);
}
}
for rel in g.relations.iter_mut() {
if rel.from == e2_name {
rel.from = e1_name.clone();
}
if rel.to == e2_name {
rel.to = e1_name.clone();
}
}
});
}
} }
} }
+9 -1
View File
@@ -22,7 +22,7 @@ pub fn error(id: serde_json::Value, code: i32, message: &str) -> serde_json::Val
pub fn tool_def<T: JsonSchema>(name: &str, fallback_desc: &str) -> serde_json::Value { pub fn tool_def<T: JsonSchema>(name: &str, fallback_desc: &str) -> serde_json::Value {
let schema = schemars::schema_for!(T); let schema = schemars::schema_for!(T);
let schema_val = serde_json::to_value(&schema).unwrap_or_default(); let mut schema_val = serde_json::to_value(&schema).unwrap_or_default();
let desc = schema_val let desc = schema_val
.get("description") .get("description")
@@ -31,6 +31,14 @@ pub fn tool_def<T: JsonSchema>(name: &str, fallback_desc: &str) -> serde_json::V
.unwrap_or(fallback_desc) .unwrap_or(fallback_desc)
.to_string(); .to_string();
// MCP clients strictly expect a top-level `type: "object"` for tool schemas.
// Tagged enums usually generate a root `oneOf` or `anyOf`.
if let Some(obj) = schema_val.as_object_mut() {
if !obj.contains_key("type") && (obj.contains_key("oneOf") || obj.contains_key("anyOf")) {
obj.insert("type".to_string(), json!("object"));
}
}
// MCP expects standard JSON schema. Schemars returns draft-07. // MCP expects standard JSON schema. Schemars returns draft-07.
json!({ json!({
"name": name, "name": name,
-5
View File
@@ -185,7 +185,6 @@ impl std::fmt::Display for TaskStatus {
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
pub struct Entity { pub struct Entity {
pub name: String, pub name: String,
#[serde(alias = "entityType", alias = "type", alias = "category")]
pub entity_type: String, pub entity_type: String,
#[serde(default)] #[serde(default)]
pub observations: Vec<String>, pub observations: Vec<String>,
@@ -207,11 +206,8 @@ pub struct Entity {
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
pub struct Relation { pub struct Relation {
#[serde(alias = "source", alias = "from_node", alias = "source_node")]
pub from: String, pub from: String,
#[serde(alias = "target", alias = "to_node", alias = "target_node")]
pub to: String, pub to: String,
#[serde(alias = "relationType", alias = "type", alias = "relation")]
pub relation_type: String, pub relation_type: String,
#[serde(default = "default_namespace")] #[serde(default = "default_namespace")]
pub namespace: String, pub namespace: String,
@@ -363,7 +359,6 @@ pub struct Adr {
pub title: String, pub title: String,
pub context: String, pub context: String,
pub decision: String, pub decision: String,
#[serde(alias = "consequences")]
pub consequence: String, pub consequence: String,
#[serde(default = "default_adr_status")] #[serde(default = "default_adr_status")]
pub status: String, pub status: String,
+1
View File
@@ -479,6 +479,7 @@ impl MemoryHandler {
register!(graph::QueryGraphPathHandler); register!(graph::QueryGraphPathHandler);
register!(graph::CreateEntitiesHandler); register!(graph::CreateEntitiesHandler);
register!(graph::UpdateEntitiesHandler);
register!(graph::CreateRelationsHandler); register!(graph::CreateRelationsHandler);
register!(graph::AddObservationsHandler); register!(graph::AddObservationsHandler);
register!(graph::DeleteEntitiesHandler); register!(graph::DeleteEntitiesHandler);
+368 -376
View File
@@ -2,23 +2,60 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Create new entities in the knowledge graph. Note: entity_type MUST ALWAYS be PascalCase (e.g. DatabaseTable, McpTool, File). /// Create new entities in the knowledge graph. Note: entity_type MUST ALWAYS be PascalCase (e.g. DatabaseTable, McpTool, File).
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct EntityInput {
pub name: String,
pub entity_type: String,
#[serde(default)]
pub observations: Vec<String>,
#[serde(default)]
pub namespace: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct CreateEntitiesTool { pub struct CreateEntitiesTool {
/// Array of entities to create. /// Array of entities to create.
pub entities: Vec<crate::models::Entity>, pub entities: Vec<EntityInput>,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct UpdateEntityInput {
/// The current name of the entity to update.
pub name: String,
/// Optional new name to rename the entity to.
pub new_name: Option<String>,
/// Optional new entity type (e.g. PascalCase).
pub new_entity_type: Option<String>,
}
/// Update existing entities in the knowledge graph (e.g., to rename an entity or change its type).
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct UpdateEntitiesTool {
/// Array of entity updates to perform.
pub updates: Vec<UpdateEntityInput>,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct RelationInput {
pub from: String,
pub to: String,
pub relation_type: String,
#[serde(default)]
pub namespace: Option<String>,
} }
/// Create new relations between entities in the knowledge graph. /// Create new relations between entities in the knowledge graph.
/// Create new relations between entities in the knowledge graph (accepts 'from' or 'source', 'to' or 'target', and 'relation_type' or 'type').
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct CreateRelationsTool { pub struct CreateRelationsTool {
/// Array of relations to create. Each relation item MUST use 'from', 'to', and 'relation_type'. /// Array of relations to create.
pub relations: Vec<crate::models::Relation>, pub relations: Vec<RelationInput>,
} }
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ObservationInput { pub struct ObservationInput {
/// The name of the entity.
pub entity_name: String, pub entity_name: String,
/// The observation contents.
pub contents: Vec<String>, pub contents: Vec<String>,
} }
@@ -38,7 +75,9 @@ pub struct DeleteEntitiesTool {
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct DeleteObservationInput { pub struct DeleteObservationInput {
/// The name of the entity.
pub entity_name: String, pub entity_name: String,
/// The observations to delete.
pub observations: Vec<String>, pub observations: Vec<String>,
} }
@@ -53,7 +92,7 @@ pub struct DeleteObservationsTool {
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct DeleteRelationsTool { pub struct DeleteRelationsTool {
/// Array of relations to delete. /// Array of relations to delete.
pub relations: Vec<crate::models::Relation>, pub relations: Vec<RelationInput>,
} }
/// Read the entire knowledge graph. WARNING: For large graphs, use search_nodes or pagination (limit, offset) to avoid context limits. /// Read the entire knowledge graph. WARNING: For large graphs, use search_nodes or pagination (limit, offset) to avoid context limits.
@@ -182,26 +221,38 @@ pub struct GetSubgraphTool {
pub max_tokens: Option<usize>, pub max_tokens: Option<usize>,
} }
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum CheckpointAction {
Create,
Restore,
List,
Delete,
}
/// Save, restore, list, or delete point-in-time memory state snapshot checkpoints. /// Save, restore, list, or delete point-in-time memory state snapshot checkpoints.
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ManageCheckpointTool { #[serde(tag = "action", rename_all = "snake_case")]
/// Action to perform: 'create', 'restore', 'list', or 'delete'. pub enum ManageCheckpointTool {
pub action: CheckpointAction, /// Save a checkpoint.
/// Unique name or ID for the snapshot checkpoint. Create {
pub name_or_id: Option<String>, /// Unique name or ID for the snapshot checkpoint.
/// Optional description of why this checkpoint was created. name_or_id: Option<String>,
pub description: Option<String>, /// Optional description of why this checkpoint was created.
/// Optional namespace filter. description: Option<String>,
pub namespace: Option<String>, /// Optional namespace filter.
namespace: Option<String>,
},
/// Restore a checkpoint.
Restore {
/// Unique name or ID for the snapshot checkpoint.
name_or_id: Option<String>,
/// Optional namespace filter.
namespace: Option<String>,
},
/// List checkpoints.
List {
/// Optional namespace filter.
namespace: Option<String>,
},
/// Delete a checkpoint.
Delete {
/// Unique name or ID for the snapshot checkpoint.
name_or_id: Option<String>,
/// Optional namespace filter.
namespace: Option<String>,
}
} }
/// Merge two entities in the knowledge graph into one. /// Merge two entities in the knowledge graph into one.
@@ -313,14 +364,19 @@ pub struct QueryGraphPathTool {
/// Define a strict checklist of acceptance criteria for a given task or feature before starting work. /// Define a strict checklist of acceptance criteria for a given task or feature before starting work.
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct SetAcceptanceCriteriaTool { pub struct SetAcceptanceCriteriaTool {
/// The title of the task.
pub task_title: String, pub task_title: String,
/// A list of acceptance criteria requirements.
pub criteria: Vec<String>, pub criteria: Vec<String>,
} }
/// Mark a previously defined acceptance criteria as met by providing cryptographic-like proof. /// Mark a previously defined acceptance criteria as met by providing cryptographic-like proof.
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct VerifyAcceptanceCriteriaTool { pub struct VerifyAcceptanceCriteriaTool {
/// The ID of the task.
pub task_id: String, pub task_id: String,
/// The specific criterion being verified.
pub criteria: String, pub criteria: String,
/// The proof or evidence that the criterion is met.
pub proof: String, pub proof: String,
} }
@@ -351,28 +407,30 @@ pub struct GetNextActionableTasksTool {
pub limit: Option<usize>, pub limit: Option<usize>,
} }
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum HypothesisAction {
Log,
Query,
}
/// Manage diagnostic hypotheses, tested evidence, and status during problem solving. /// Manage diagnostic hypotheses, tested evidence, and status during problem solving.
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct HypothesesTool { #[serde(tag = "action", rename_all = "snake_case")]
/// Action to perform: 'log' or 'query'. pub enum HypothesesTool {
pub action: HypothesisAction, /// Log a new diagnostic hypothesis.
/// Optional task ID associated with this hypothesis or to filter hypotheses. Log {
pub task_id: Option<String>, /// The diagnostic hypothesis or potential root cause.
/// The diagnostic hypothesis or potential root cause (required for action 'log'). hypothesis: String,
pub hypothesis: Option<String>, /// Optional task ID associated with this hypothesis.
/// Status: 'unverified', 'verified', or 'rejected'. Defaults to 'unverified'. task_id: Option<String>,
pub status: Option<String>, /// Status: 'unverified', 'verified', or 'rejected'. Defaults to 'unverified'.
/// Evidence or test results supporting or disproving the hypothesis. status: Option<String>,
pub evidence: Option<String>, /// Evidence or test results supporting or disproving the hypothesis.
/// Optional search query text (for action 'query'). evidence: Option<String>,
pub query: Option<String>, },
/// Query existing hypotheses.
Query {
/// Optional search query text.
query: Option<String>,
/// Optional task ID to filter hypotheses.
task_id: Option<String>,
/// Optional status filter.
status: Option<String>,
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
@@ -434,27 +492,35 @@ pub struct ProcessLogsTool {
/// Read a file and return only its AST skeleton (Imports, Structs, Enums, Traits, Functions) /// Read a file and return only its AST skeleton (Imports, Structs, Enums, Traits, Functions)
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ReadFileSkeletonTool { pub struct ReadFileSkeletonTool {
/// The path to the file to read.
pub file_path: String, pub file_path: String,
} }
/// Replace a specific AST node in a file (robust structural editing). Use this instead of regex or line-based string replacement to prevent indentation bugs and matching failures. /// Replace a specific AST node in a file (robust structural editing). Use this instead of regex or line-based string replacement to prevent indentation bugs and matching failures.
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ReplaceAstNodeTool { pub struct ReplaceAstNodeTool {
/// The path of the file to modify.
pub file_path: String, pub file_path: String,
pub node_type: String, // e.g., "function_item", "impl_item" /// The AST node type to replace (e.g., 'function_item', 'impl_item').
pub node_name: String, // e.g., "execute" pub node_type: String,
/// The name of the specific AST node to replace (e.g., 'execute').
pub node_name: String,
/// The completely new structural content for this AST node.
pub new_content: String, pub new_content: String,
} }
/// Semantic code search using local vector embeddings. Use this conceptual search instead of raw regex (grep) when trying to locate abstract logic or exploring new patterns. /// Semantic code search using local vector embeddings. Use this conceptual search instead of raw regex (grep) when trying to locate abstract logic or exploring new patterns.
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct SemanticCodeSearchTool { pub struct SemanticCodeSearchTool {
/// The conceptual or semantic search query.
pub query: String, pub query: String,
/// Optional directory to restrict the search to.
pub directory: Option<String>, pub directory: Option<String>,
} }
/// Get a bird's-eye view of directory architecture. Use this when first exploring a new repository to get a summary of what each file is responsible for, instead of blindly reading files. /// Get a bird's-eye view of directory architecture. Use this when first exploring a new repository to get a summary of what each file is responsible for, instead of blindly reading files.
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ReadDirectoryArchitectureTool { pub struct ReadDirectoryArchitectureTool {
/// The directory path to analyze.
pub directory: String, pub directory: String,
} }
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] #[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
@@ -560,28 +626,30 @@ pub struct SummarizeSubgraphTool {
pub max_tokens: Option<usize>, pub max_tokens: Option<usize>,
} }
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum AgentSignalAction {
Broadcast,
Query,
}
/// Real-time inter-agent communication bus: broadcast signals or query active signals from peer subagents. /// Real-time inter-agent communication bus: broadcast signals or query active signals from peer subagents.
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct AgentSignalsTool { #[serde(tag = "action", rename_all = "snake_case")]
/// Action to perform: 'broadcast' or 'query'. pub enum AgentSignalsTool {
pub action: AgentSignalAction, /// Broadcast a signal to peer subagents.
/// Sender agent ID or role (e.g. 'PrePushAuditor', 'MemoryLibrarian'). Required for broadcast; optional filter for query. Broadcast {
pub sender: Option<String>, /// Sender agent ID or role (e.g. 'PrePushAuditor', 'MemoryLibrarian').
/// Signal type or event category (e.g. 'AUDIT_PASSED', 'REPRODUCER_READY', 'TESTS_FAILED'). Required for broadcast; optional filter for query. sender: String,
pub signal_type: Option<String>, /// Signal type or event category (e.g. 'AUDIT_PASSED', 'REPRODUCER_READY', 'TESTS_FAILED').
/// JSON or text payload containing event details or artifact URIs (required for action 'broadcast'). signal_type: String,
pub payload: Option<String>, /// JSON or text payload containing event details or artifact URIs.
/// Optional Time-To-Live in seconds for the signal. Defaults to 3600 (1 hour). payload: String,
pub ttl_seconds: Option<u64>, /// Optional Time-To-Live in seconds for the signal. Defaults to 3600 (1 hour).
/// Optional limit on returned signals (for action 'query'). Defaults to 20. ttl_seconds: Option<u64>,
pub limit: Option<usize>, },
/// Query active signals.
Query {
/// Optional sender agent ID or role filter.
sender: Option<String>,
/// Optional signal type filter.
signal_type: Option<String>,
/// Optional limit on returned signals. Defaults to 20.
limit: Option<usize>,
}
} }
/// Trigger an automated context checkpoint, summarizing active tasks, hypotheses, recent commits, and open tech debt into a permanent HandoffMemo. /// Trigger an automated context checkpoint, summarizing active tasks, hypotheses, recent commits, and open tech debt into a permanent HandoffMemo.
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
@@ -597,21 +665,12 @@ pub struct AutoSessionCheckpointTool {
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] #[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum HandoffMemoAction { pub enum HandoffMemoAction {
#[serde(alias = "leave", alias = "LEAVE", alias = "Leave")]
Leave, Leave,
#[serde(alias = "read", alias = "READ", alias = "Read")]
Read, Read,
#[serde(alias = "clear", alias = "CLEAR", alias = "Clear")]
Clear, Clear,
} }
/// Persistent handoff memos for passing session context to future agents. /// Persistent handoff memos for passing session context to future agents.
/// Actions:
/// - 'leave': Post a handoff memo. Required: content. Optional: namespace.
/// - 'read': Inspect active memos. Optional: namespace.
/// - 'clear': Clear memos. Optional: namespace, ids.
///
/// Next steps on error: Check parameter requirements for 'leave' or 'clear' actions.
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct HandoffMemosTool { pub struct HandoffMemosTool {
/// Action to perform: 'leave', 'read', or 'clear'. /// Action to perform: 'leave', 'read', or 'clear'.
@@ -624,150 +683,124 @@ pub struct HandoffMemosTool {
pub ids: Option<Vec<String>>, pub ids: Option<Vec<String>>,
} }
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] /// Action-oriented task tracking system.
#[serde(rename_all = "snake_case")]
pub enum TaskAction {
#[serde(alias = "add", alias = "ADD", alias = "Add")]
Add,
#[serde(alias = "update", alias = "UPDATE", alias = "Update")]
Update,
#[serde(alias = "delete", alias = "DELETE", alias = "Delete")]
Delete,
#[serde(alias = "list", alias = "LIST", alias = "List")]
List,
#[serde(
alias = "set_criteria",
alias = "setCriteria",
alias = "SET_CRITERIA",
alias = "SetCriteria"
)]
SetCriteria,
#[serde(alias = "verify", alias = "VERIFY", alias = "Verify")]
Verify,
}
/// Action-oriented task tracking system (add, update, delete, list, set_criteria, verify).
/// Actions:
/// - 'add': Create task. Required: title. Optional: description, git_branch, parent_id, dependencies.
/// - 'update': Update task status. Required: id, status ('pending'|'completed'|'cancelled').
/// - 'delete': Delete task & subtasks. Required: id.
/// - 'list': List active tasks. Optional: git_branch, summary_level ('compact'|'detailed'|'full'), max_tokens.
/// - 'set_criteria': Set acceptance criteria. Required: id, criteria (array of strings).
/// - 'verify': Verify criteria met. Required: id.
///
/// Next steps on error: Check required parameters or call list to verify task IDs.
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct TasksTool { #[serde(tag = "action", rename_all = "snake_case")]
/// Action to perform: 'add', 'update', 'delete', 'list', 'set_criteria', or 'verify'. pub enum TasksTool {
pub action: TaskAction, /// Create a new task.
/// Task ID (required for 'update', 'delete', 'set_criteria', 'verify'). Add {
#[serde(alias = "task_id", alias = "taskId")] /// Task title.
pub id: Option<String>, title: String,
/// Task title (required for 'add'). /// Task description.
#[serde(alias = "name")] description: Option<String>,
pub title: Option<String>, /// Optional parent task ID for subtasks.
/// Task description (optional for 'add'). parent_id: Option<String>,
#[serde(alias = "desc")] /// List of dependent task IDs.
pub description: Option<String>, dependencies: Option<Vec<String>>,
/// New status: 'pending', 'completed', or 'cancelled' (for 'update'). /// Branch associated with this task.
pub status: Option<String>, git_branch: Option<String>,
/// Parent task ID (optional for 'add'). /// Optional namespace filter or assignment.
#[serde(alias = "parentId", alias = "parent")] namespace: Option<String>,
pub parent_id: Option<String>, /// Repository name.
/// List of dependency task IDs (optional for 'add'). repo_name: Option<String>,
#[serde(alias = "deps")] /// Priority: 'low', 'medium', 'high', 'critical'.
pub dependencies: Option<Vec<String>>, priority: Option<String>,
/// Git branch filter or assignment. /// Assigned agent or subagent.
#[serde(alias = "branch")] assigned_agent: Option<String>,
pub git_branch: Option<String>, /// Command used to verify completion.
/// Acceptance criteria (required for 'set_criteria'). verification_command: Option<String>,
#[serde(alias = "acceptance_criteria", alias = "acceptanceCriteria")] },
pub criteria: Option<Vec<String>>, /// Update task status.
/// Verification proof or details (optional for 'verify'). Update {
pub proof: Option<String>, /// Task ID.
/// Summary level: 'compact', 'detailed', or 'full' (for 'list'). id: String,
pub summary_level: Option<String>, /// New status: 'pending', 'completed', or 'cancelled'.
/// Maximum tokens budget cap (for 'list'). status: String,
pub max_tokens: Option<usize>, },
/// Optional namespace filter or assignment (defaults to 'default'). /// Delete a task and its subtasks.
pub namespace: Option<String>, Delete {
/// Optional repository name. /// Task ID.
pub repo_name: Option<String>, id: String,
/// Optional task priority: 'low', 'medium', 'high', 'urgent'. },
pub priority: Option<String>, /// List active tasks.
/// Optional assigned subagent role or identifier. List {
pub assigned_agent: Option<String>, /// Optional Git branch filter.
/// Optional verification command to validate criteria completion. git_branch: Option<String>,
/// Optional pagination offset for 'list'. /// Optional status filter.
pub offset: Option<usize>, status: Option<String>,
pub verification_command: Option<String>, /// Summary level: 'compact', 'detailed', or 'full'. Defaults to 'detailed'.
summary_level: Option<String>,
/// Maximum tokens budget cap.
max_tokens: Option<usize>,
/// Optional pagination offset.
offset: Option<usize>,
},
/// Set a checklist of acceptance criteria.
SetCriteria {
/// Task ID or title.
id: String,
/// Acceptance criteria list.
criteria: Vec<String>,
},
/// Mark an acceptance criteria as met with cryptographic-like proof.
Verify {
/// Task ID or title.
id: String,
/// Verification proof or details.
proof: Option<String>,
}
} }
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] /// Project milestone management.
#[serde(rename_all = "snake_case")]
pub enum MilestoneAction {
#[serde(alias = "add", alias = "ADD", alias = "Add")]
Add,
#[serde(alias = "update", alias = "UPDATE", alias = "Update")]
Update,
#[serde(alias = "list", alias = "LIST", alias = "List")]
List,
}
/// Project milestone management (add, update, list).
/// Actions:
/// - 'add': Create milestone. Required: title. Optional: namespace.
/// - 'update': Update milestone status. Required: id, status.
/// - 'list': List milestones. Optional: namespace.
///
/// Next steps on error: Provide required title for 'add' or id/status for 'update'.
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct MilestonesTool { #[serde(tag = "action", rename_all = "snake_case")]
/// Action to perform: 'add', 'update', or 'list'. pub enum MilestonesTool {
pub action: MilestoneAction, /// Create a new milestone.
/// Milestone ID (required for 'update'). Add {
#[serde(alias = "milestone_id", alias = "milestoneId")] /// Milestone title.
pub id: Option<String>, title: String,
/// Milestone title (required for 'add'). /// Optional namespace filter or assignment.
#[serde(alias = "name")] namespace: Option<String>,
pub title: Option<String>, /// Optional target completion date.
/// New status (for 'update'). target_date: Option<String>,
pub status: Option<String>, /// Optional description of milestone scope.
/// Optional namespace filter or assignment. description: Option<String>,
pub namespace: Option<String>, /// Optional deliverables or task checklist.
/// Optional target completion date. deliverables: Option<Vec<String>>,
pub target_date: Option<String>, /// Optional list of task IDs associated with this milestone.
/// Optional description of milestone scope. #[serde(default)]
pub description: Option<String>, task_ids: Option<Vec<String>>,
/// Optional deliverables or task checklist. /// Optional repository name.
pub deliverables: Option<Vec<String>>, repo_name: Option<String>,
/// Optional list of task IDs associated with this milestone. },
#[serde(default)] /// Update milestone status.
pub task_ids: Option<Vec<String>>, Update {
/// Optional repository name. /// Milestone ID.
pub repo_name: Option<String>, id: String,
/// New status.
status: String,
/// Optional deliverables or task checklist.
deliverables: Option<Vec<String>>,
/// Optional list of task IDs associated with this milestone.
#[serde(default)]
task_ids: Option<Vec<String>>,
},
/// List milestones.
List {
/// Optional namespace.
namespace: Option<String>,
}
} }
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] #[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum SnippetAction { pub enum SnippetAction {
#[serde(alias = "store", alias = "STORE", alias = "Store")]
Store, Store,
#[serde(alias = "search", alias = "SEARCH", alias = "Search")]
Search, Search,
#[serde(alias = "delete", alias = "DELETE", alias = "Delete")]
Delete, Delete,
#[serde(alias = "tag", alias = "TAG", alias = "Tag")]
Tag, Tag,
} }
/// Syntactically preserved reusable code snippets vault (store, search, delete, tag). /// Syntactically preserved reusable code snippets vault (store, search, delete, tag).
/// Actions:
/// - 'store': Store snippet. Required: query (or id) as snippet name. Optional: language, code, description, tags.
/// - 'search': Search snippets. Optional: query, tags, hybrid (boolean for BM25+vector search).
/// - 'delete': Delete snippet. Required: id (or query) as snippet name.
/// - 'tag': Tag snippet. Required: id (or query), tags (array of strings).
///
/// Next steps on error: Ensure snippet name/query or id is provided.
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct SnippetsTool { pub struct SnippetsTool {
/// Action to perform: 'store', 'search', 'delete', or 'tag'. /// Action to perform: 'store', 'search', 'delete', or 'tag'.
@@ -798,177 +831,154 @@ pub struct SnippetsTool {
pub line_range: Option<String>, pub line_range: Option<String>,
} }
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq, Clone)] /// Architectural Decision Records (ADRs) log.
#[serde(rename_all = "snake_case")]
pub enum DecisionAction {
#[serde(alias = "log", alias = "LOG", alias = "Log")]
Log,
#[serde(alias = "query", alias = "QUERY", alias = "Query")]
Query,
#[serde(alias = "delete", alias = "DELETE", alias = "Delete")]
Delete,
#[serde(alias = "update", alias = "UPDATE", alias = "Update")]
Update,
}
/// Architectural Decision Records (ADRs) log (log, query, update, delete).
/// Actions:
/// - 'log': Record ADR. Required: title. Optional: status, context, decision, consequences, namespace.
/// - 'query': Search ADRs. Optional: query, namespace.
/// - 'update': Update ADR status or implementation info. Required: id. Optional: status, git_commit, git_branch, task_id, context, decision, consequences, title.
/// - 'delete': Remove ADR. Required: id.
///
/// Next steps on error: Provide title for 'log' or id for 'update'/'delete'.
#[derive(Deserialize, Serialize, JsonSchema, Debug, Clone)] #[derive(Deserialize, Serialize, JsonSchema, Debug, Clone)]
pub struct DecisionsTool { #[serde(tag = "action", rename_all = "snake_case")]
/// Action to perform: 'log', 'query', or 'delete'. pub enum DecisionsTool {
pub action: DecisionAction, /// Record an ADR.
/// ADR ID (required for 'delete'). Log {
pub id: Option<String>, /// ADR title.
/// ADR title (required for 'log'). title: String,
pub title: Option<String>, /// ADR status.
/// ADR status (for 'log'). status: Option<String>,
pub status: Option<String>, /// Context & problem statement.
/// Context & problem statement (for 'log'). context: Option<String>,
pub context: Option<String>, /// Decision made.
/// Decision made (for 'log'). decision: Option<String>,
pub decision: Option<String>, /// Consequences & tradeoffs.
/// Consequences & tradeoffs (for 'log'). consequences: Option<String>,
#[serde(alias = "consequence")] /// Optional namespace.
pub consequences: Option<String>, namespace: Option<String>,
/// Search query string (for 'query'). /// Optional repository name.
pub query: Option<String>, repo_name: Option<String>,
/// Optional namespace. /// Optional author or architect behind the decision.
pub namespace: Option<String>, author: Option<String>,
/// Optional repository name. /// Optional affected system components or crates.
pub repo_name: Option<String>, affected_components: Option<Vec<String>>,
/// Optional author or architect behind the decision. /// Optional alternative designs or libraries considered.
pub author: Option<String>, alternatives_considered: Option<Vec<String>>,
/// Optional affected system components or crates. /// Optional superseded ADR ID.
pub affected_components: Option<Vec<String>>, supersedes: Option<String>,
/// Optional alternative designs or libraries considered. /// Optional Git commit where decision was implemented.
pub alternatives_considered: Option<Vec<String>>, git_commit: Option<String>,
/// Optional superseded ADR ID. /// Optional Git branch where decision was implemented.
pub supersedes: Option<String>, git_branch: Option<String>,
/// Limit the number of query results. Defaults to 20. /// Optional task ID linked to this decision.
pub limit: Option<usize>, task_id: Option<String>,
/// Include the full body of the matched decisions. Defaults to true. },
pub include_body: Option<bool>, /// Search ADRs.
/// Optional Git commit where decision was implemented. Query {
pub git_commit: Option<String>, /// Search query string.
/// Optional Git branch where decision was implemented. query: Option<String>,
pub git_branch: Option<String>, /// Optional namespace.
/// Optional task ID linked to this decision. namespace: Option<String>,
pub task_id: Option<String>, /// Limit the number of query results. Defaults to 20.
limit: Option<usize>,
/// Include the full body of the matched decisions. Defaults to true.
include_body: Option<bool>,
},
/// Update ADR status or implementation info.
Update {
/// ADR ID.
id: String,
/// ADR title.
title: Option<String>,
/// ADR status.
status: Option<String>,
/// Context & problem statement.
context: Option<String>,
/// Decision made.
decision: Option<String>,
/// Consequences & tradeoffs.
consequences: Option<String>,
/// Optional Git commit where decision was implemented.
git_commit: Option<String>,
/// Optional Git branch where decision was implemented.
git_branch: Option<String>,
/// Optional task ID linked to this decision.
task_id: Option<String>,
/// Optional namespace.
namespace: Option<String>,
/// Optional repository name.
repo_name: Option<String>,
/// Optional author or architect behind the decision.
author: Option<String>,
/// Optional affected system components or crates.
affected_components: Option<Vec<String>>,
/// Optional alternative designs or libraries considered.
alternatives_considered: Option<Vec<String>>,
/// Optional superseded ADR ID.
supersedes: Option<String>,
},
/// Remove an ADR.
Delete {
/// ADR ID.
id: String,
}
} }
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] /// Technical debt backlog management.
#[serde(rename_all = "snake_case")]
pub enum TechDebtAction {
#[serde(alias = "log", alias = "LOG", alias = "Log")]
Log,
#[serde(alias = "resolve", alias = "RESOLVE", alias = "Resolve")]
Resolve,
#[serde(alias = "list", alias = "LIST", alias = "List")]
List,
}
/// Technical debt backlog management (log, resolve, list).
/// Actions:
/// - 'log': Record technical debt. Required: description. Optional: ideal_solution, git_commit, git_branch, symbol_references, line_range, namespace.
/// - 'resolve': Mark tech debt resolved. Required: id.
/// - 'list': List tech debt items. Optional: include_resolved, namespace.
///
/// Next steps on error: Provide description for 'log' or valid ID for 'resolve'.
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct TechDebtTool { #[serde(tag = "action", rename_all = "snake_case")]
/// Action to perform: 'log', 'resolve', or 'list'. pub enum TechDebtTool {
pub action: TechDebtAction, /// Record technical debt.
/// Tech debt ID (required for 'resolve'). Log {
#[serde(alias = "tech_debt_id", alias = "debt_id")] /// Tech debt description.
pub id: Option<String>, description: String,
/// Tech debt description (required for 'log'). /// Optional title or summary of tech debt.
#[serde(alias = "desc")] title: Option<String>,
pub description: Option<String>, /// Ideal solution.
/// Optional title or summary of tech debt. ideal_solution: Option<String>,
pub title: Option<String>, /// Associated git commit hash.
/// Ideal solution (optional for 'log'). git_commit: Option<String>,
#[serde(alias = "solution", alias = "fix")] /// Associated git branch.
pub ideal_solution: Option<String>, git_branch: Option<String>,
/// Include resolved tech debt items (for 'list'). /// Symbol references associated with the tech debt.
pub include_resolved: Option<bool>, symbol_references: Option<Vec<String>>,
/// Associated git commit hash. /// Line range associated with the tech debt.
#[serde(alias = "commit")] line_range: Option<String>,
pub git_commit: Option<String>, /// Optional namespace.
/// Associated git branch. namespace: Option<String>,
#[serde(alias = "branch")] /// Optional debt severity: 'low', 'medium', 'high', 'critical'.
pub git_branch: Option<String>, severity: Option<String>,
/// Symbol references associated with the tech debt. /// Optional repository name.
pub symbol_references: Option<Vec<String>>, repo_name: Option<String>,
/// Line range associated with the tech debt. /// Optional file path where debt exists.
pub line_range: Option<String>, file_path: Option<String>,
/// Optional namespace. /// Optional workaround currently in place.
pub namespace: Option<String>, workaround: Option<String>,
/// Optional debt severity: 'low', 'medium', 'high', 'critical'. /// Optional effort estimate to fix.
pub severity: Option<String>, effort_estimate: Option<String>,
/// Optional repository name. },
pub repo_name: Option<String>, /// Mark tech debt resolved.
/// Optional file path where debt exists. Resolve {
pub file_path: Option<String>, /// Tech debt ID.
/// Optional workaround currently in place. id: String,
pub workaround: Option<String>, },
/// Optional effort estimate to fix. /// List tech debt items.
pub effort_estimate: Option<String>, List {
/// Summary level for list action: 'compact', 'detailed', or 'full'. /// Include resolved tech debt items.
pub summary_level: Option<String>, include_resolved: Option<bool>,
/// Optional token budget cap for list output. /// Optional namespace.
pub max_tokens: Option<usize>, namespace: Option<String>,
/// Summary level for list action: 'compact', 'detailed', or 'full'.
summary_level: Option<String>,
/// Optional token budget cap for list output.
max_tokens: Option<usize>,
}
} }
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] #[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum EnvAction { pub enum EnvAction {
#[serde(
alias = "update_fingerprint",
alias = "updateFingerprint",
alias = "UPDATE_FINGERPRINT",
alias = "UpdateFingerprint"
)]
UpdateFingerprint, UpdateFingerprint,
#[serde(
alias = "read_fingerprint",
alias = "readFingerprint",
alias = "READ_FINGERPRINT",
alias = "ReadFingerprint"
)]
ReadFingerprint, ReadFingerprint,
#[serde(
alias = "log_requirement",
alias = "logRequirement",
alias = "LOG_REQUIREMENT",
alias = "LogRequirement"
)]
LogRequirement, LogRequirement,
#[serde(alias = "register", alias = "REGISTER", alias = "Register")]
Register, Register,
#[serde(
alias = "get_details",
alias = "getDetails",
alias = "GET_DETAILS",
alias = "GetDetails"
)]
GetDetails, GetDetails,
} }
/// Environment requirements, tool fingerprints & infrastructure management. /// Environment requirements, tool fingerprints & infrastructure management.
/// Actions:
/// - 'update_fingerprint': Update tool versions. Optional: tool_versions map, namespace.
/// - 'read_fingerprint': Read current environment fingerprint. Optional: namespace.
/// - 'log_requirement': Log environment variable requirement. Required: key. Optional: description, is_secret, namespace.
/// - 'register': Register remote target environment. Required: name. Optional: url, description, requires_vpn, namespace.
/// - 'get_details': Get full environment details. Optional: namespace.
///
/// Next steps on error: Provide key for 'log_requirement' or name for 'register'.
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct EnvironmentTool { pub struct EnvironmentTool {
/// Action to perform: 'update_fingerprint', 'read_fingerprint', 'log_requirement', 'register', or 'get_details'. /// Action to perform: 'update_fingerprint', 'read_fingerprint', 'log_requirement', 'register', or 'get_details'.
@@ -998,37 +1008,19 @@ pub struct EnvironmentTool {
#[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema, PartialEq)] #[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema, PartialEq)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum ClipboardAction { pub enum ClipboardAction {
#[serde(
alias = "image",
alias = "IMAGE",
alias = "Image",
alias = "screenshot",
alias = "SCREENSHOT"
)]
Image, Image,
#[serde(alias = "text", alias = "TEXT", alias = "Text")]
Text, Text,
#[serde(alias = "history", alias = "HISTORY", alias = "History")]
History, History,
#[serde(alias = "clear", alias = "CLEAR", alias = "Clear")]
Clear, Clear,
#[serde(alias = "write", alias = "WRITE", alias = "Write")]
Write, Write,
} }
/// Smart OS Clipboard management with overwrite-immune screenshot caching and OCR. /// Smart OS Clipboard management with overwrite-immune screenshot caching and OCR.
/// Actions:
/// - 'image': Get latest screenshot image path (Windows + WSL) and extracted verbatim OCR text.
/// Returns cached screenshot even if text was copied afterwards! /// Returns cached screenshot even if text was copied afterwards!
/// - 'text': Get latest clipboard text (or normalized Markdown if HTML was copied).
/// - 'history': View recent clipboard history ring buffer (images and text with timestamps).
/// - 'clear': Clear OS clipboard and memory cache.
/// - 'write': Write content to OS clipboard. Optional: text, html, files, image_path.
///
/// Triggers: Call 'image' immediately when user says "look at image in clipboard", "see screenshot", /// Triggers: Call 'image' immediately when user says "look at image in clipboard", "see screenshot",
/// "look at clipboard", "what I copied", or shares terminal error captures. /// "look at clipboard", "what I copied", or shares terminal error captures.
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Debug, Deserialize, Serialize, JsonSchema)]
pub struct ClipboardTool { pub struct ClipboardTool {
/// Action to perform: 'image', 'text', 'history', 'clear', 'read', or 'write'. /// Action to perform: 'image', 'text', 'history', 'clear', or 'write'.
pub action: ClipboardAction, pub action: ClipboardAction,
/// Plain text content (for 'write'). /// Plain text content (for 'write').
pub text: Option<String>, pub text: Option<String>,
+4 -5
View File
@@ -29,11 +29,10 @@ fn test_eager_tools_parity() {
let nvim_source = let nvim_source =
std::fs::read_to_string("../nvim-core/src/lib.rs").expect("Failed to read nvim lib.rs"); std::fs::read_to_string("../nvim-core/src/lib.rs").expect("Failed to read nvim lib.rs");
let mut nvim_tools = HashSet::new(); let mut nvim_tools = HashSet::new();
for line in nvim_source.lines() { let parts: Vec<&str> = nvim_source.split("tool_def").collect();
if line.contains("\"name\": \"nvim_") for part in parts.iter().skip(1) {
&& let Some(start) = line.find("\"name\": \"") if let Some(start) = part.find("(\"") {
{ let rest = &part[start + 2..];
let rest = &line[start + 9..];
if let Some(end) = rest.find('"') { if let Some(end) = rest.find('"') {
nvim_tools.insert(rest[..end].to_string()); nvim_tools.insert(rest[..end].to_string());
} }
View File
Whitespace-only changes.