Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c326166311 | ||
|
|
5311167898 |
No files matched your search
Generated
+4
-40
@@ -2651,7 +2651,7 @@ dependencies = [
|
|||||||
"reqwest",
|
"reqwest",
|
||||||
"rmcp",
|
"rmcp",
|
||||||
"rten",
|
"rten",
|
||||||
"schemars 1.2.2",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
@@ -3009,7 +3009,6 @@ dependencies = [
|
|||||||
"mcp-stdio",
|
"mcp-stdio",
|
||||||
"rmcp",
|
"rmcp",
|
||||||
"rmpv",
|
"rmpv",
|
||||||
"schemars 0.8.22",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -3860,7 +3859,7 @@ dependencies = [
|
|||||||
"pastey 0.2.3",
|
"pastey 0.2.3",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rmcp-macros",
|
"rmcp-macros",
|
||||||
"schemars 1.2.2",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
@@ -4180,18 +4179,6 @@ 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"
|
||||||
@@ -4201,23 +4188,11 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"dyn-clone",
|
"dyn-clone",
|
||||||
"ref-cast",
|
"ref-cast",
|
||||||
"schemars_derive 1.2.2",
|
"schemars_derive",
|
||||||
"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"
|
||||||
@@ -4226,7 +4201,7 @@ checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"serde_derive_internals 0.30.0",
|
"serde_derive_internals",
|
||||||
"syn 3.0.6",
|
"syn 3.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4301,17 +4276,6 @@ 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"
|
||||||
|
|||||||
@@ -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 `consequences`.
|
- When selecting design patterns, crate choices, or system structure, call `decisions` (action: "log") with `author`, `affected_components`, `alternatives_considered`, `decision`, and `consequence`.
|
||||||
- **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 `criteria`.
|
- **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`.
|
||||||
- **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.
|
||||||
|
|||||||
@@ -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 the markdown natively in the editor (e.g., `Antigravity_Plan.md` scratch buffer). 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 a vertical split (e.g., `Antigravity_Plan.md` scratch buffer) and present the markdown natively. 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:
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ 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"
|
||||||
|
|||||||
+932
-260
File diff suppressed because it is too large.
Load diff
@@ -1,101 +0,0 @@
|
|||||||
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 },
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
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());
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
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
@@ -1,5 +1,5 @@
|
|||||||
use crate::store::STORE_TABLE;
|
use crate::store::STORE_TABLE;
|
||||||
use redb::Database;
|
use redb::{Database, ReadableTable};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|||||||
+29
-167
@@ -163,15 +163,9 @@ 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 entity_input in req.entities {
|
for mut entity in req.entities {
|
||||||
if !entity_input.name.is_empty() {
|
if !entity.name.is_empty() {
|
||||||
let entity = crate::models::Entity {
|
entity.entity_type = crate::models::normalize_entity_type(&entity.entity_type);
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -193,108 +187,6 @@ 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]
|
||||||
@@ -364,15 +256,10 @@ impl McpTool for CreateRelationsHandler {
|
|||||||
auto_created.push(node_name);
|
auto_created.push(node_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for relation_input in req.relations {
|
for mut relation in req.relations {
|
||||||
if !relation_input.from.is_empty() && !relation_input.to.is_empty() {
|
if !relation.from.is_empty() && !relation.to.is_empty() {
|
||||||
let relation = crate::models::Relation {
|
relation.relation_type =
|
||||||
from: relation_input.from,
|
crate::models::normalize_relation_type(&relation.relation_type);
|
||||||
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
|
||||||
@@ -511,48 +398,30 @@ 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let mut missing_entities = Vec::new();
|
if !missing.is_empty() {
|
||||||
let mut missing_observations = Vec::new();
|
return Err(crate::error::AppError::Internal(format!(
|
||||||
|
"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) {
|
||||||
for target in d.observations {
|
let to_rem: HashSet<_> = d.observations.into_iter().collect();
|
||||||
let target_clean = target.trim();
|
e.observations.retain(|o| !to_rem.contains(o));
|
||||||
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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -574,21 +443,13 @@ 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 deleted_count = 0;
|
let mut missing_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| {
|
master.relations.retain(|r| !to_rem.contains(r));
|
||||||
let should_delete = req.relations.iter().any(|target| {
|
missing_count = to_rem.len() - (initial_len - master.relations.len());
|
||||||
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.",
|
||||||
@@ -1508,9 +1369,10 @@ 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": [
|
||||||
{"from": "A", "to": "B", "relation_type": "knows"}
|
{"source": "A", "target": "B", "relationType": "knows"}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
let alias_res = handler.execute(alias_args, state.clone()).await.unwrap();
|
let alias_res = handler.execute(alias_args, state.clone()).await.unwrap();
|
||||||
|
|||||||
+172
-98
@@ -24,7 +24,24 @@ 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 solution = req.solution;
|
let mut solution = req.solution;
|
||||||
|
|
||||||
|
if state.ollama.is_available().await {
|
||||||
|
let prompt = format!(
|
||||||
|
"Analyze this error signature and solution. Output 1 sentence summarizing the root cause and fix:\nSignature: {}\nSolution: {}",
|
||||||
|
req.signature, solution
|
||||||
|
);
|
||||||
|
if let Ok(summary) = state
|
||||||
|
.ollama
|
||||||
|
.generate(&prompt, Some(&state.ollama.reasoning_model), None, 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
|
||||||
@@ -187,6 +204,18 @@ 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()
|
||||||
@@ -354,14 +383,18 @@ 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())?;
|
||||||
match req {
|
let ns = req
|
||||||
DecisionsTool::Log { title, status, context, decision, consequences, namespace, repo_name, author, affected_components, alternatives_considered, supersedes, git_commit, git_branch, task_id } => {
|
.namespace
|
||||||
let ns = namespace.clone().unwrap_or_else(crate::models::default_namespace);
|
.unwrap_or_else(crate::models::default_namespace);
|
||||||
let title = title;
|
match req.action {
|
||||||
let status = status.unwrap_or_else(|| "accepted".to_string());
|
DecisionAction::Log => {
|
||||||
let context = context.unwrap_or_default();
|
let title = req.title.ok_or_else(|| {
|
||||||
let decision = decision.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 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())
|
||||||
@@ -374,7 +407,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) = &supersedes {
|
if let Some(superseded_id) = &req.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();
|
||||||
@@ -392,17 +425,17 @@ impl McpTool for DecisionsHandler {
|
|||||||
decision: decision.clone(),
|
decision: decision.clone(),
|
||||||
consequence,
|
consequence,
|
||||||
status,
|
status,
|
||||||
supersedes: supersedes,
|
supersedes: req.supersedes,
|
||||||
timestamp: crate::handlers::utils::now_secs(),
|
timestamp: crate::handlers::utils::now_secs(),
|
||||||
namespace: ns,
|
namespace: ns,
|
||||||
repo_name: repo_name,
|
repo_name: req.repo_name,
|
||||||
alternatives_considered: alternatives_considered.unwrap_or_default(),
|
alternatives_considered: req.alternatives_considered.unwrap_or_default(),
|
||||||
affected_components: affected_components.unwrap_or_default(),
|
affected_components: req.affected_components.unwrap_or_default(),
|
||||||
author: author,
|
author: req.author,
|
||||||
git_commit: git_commit,
|
git_commit: req.git_commit,
|
||||||
git_branch: git_branch,
|
git_branch: req.git_branch,
|
||||||
resolved_at,
|
resolved_at,
|
||||||
task_id: task_id,
|
task_id: req.task_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
adrs_to_index.push(a.clone());
|
adrs_to_index.push(a.clone());
|
||||||
@@ -420,15 +453,17 @@ impl McpTool for DecisionsHandler {
|
|||||||
);
|
);
|
||||||
Ok(format!("Logged decision {}: {}", final_id, title))
|
Ok(format!("Logged decision {}: {}", final_id, title))
|
||||||
}
|
}
|
||||||
DecisionsTool::Update { id, title, status, context, decision, consequences, git_commit, git_branch, task_id, namespace: _, repo_name, author, affected_components, alternatives_considered, supersedes } => {
|
DecisionAction::Update => {
|
||||||
let id = id;
|
let id = req.id.ok_or_else(|| {
|
||||||
|
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) = &supersedes
|
if let Some(superseded_id) = &req.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))
|
||||||
@@ -439,19 +474,19 @@ impl McpTool for DecisionsHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let a = &mut adrs[pos];
|
let a = &mut adrs[pos];
|
||||||
if let Some(t) = title {
|
if let Some(t) = req.title {
|
||||||
a.title = t;
|
a.title = t;
|
||||||
}
|
}
|
||||||
if let Some(c) = context {
|
if let Some(c) = req.context {
|
||||||
a.context = c;
|
a.context = c;
|
||||||
}
|
}
|
||||||
if let Some(d) = decision {
|
if let Some(d) = req.decision {
|
||||||
a.decision = d;
|
a.decision = d;
|
||||||
}
|
}
|
||||||
if let Some(cons) = consequences {
|
if let Some(cons) = req.consequences {
|
||||||
a.consequence = cons;
|
a.consequence = cons;
|
||||||
}
|
}
|
||||||
if let Some(s) = status {
|
if let Some(s) = req.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()
|
||||||
@@ -462,29 +497,29 @@ impl McpTool for DecisionsHandler {
|
|||||||
}
|
}
|
||||||
a.status = s;
|
a.status = s;
|
||||||
}
|
}
|
||||||
if supersedes.is_some() {
|
if req.supersedes.is_some() {
|
||||||
a.supersedes = supersedes;
|
a.supersedes = req.supersedes;
|
||||||
}
|
}
|
||||||
if repo_name.is_some() {
|
if req.repo_name.is_some() {
|
||||||
a.repo_name = repo_name;
|
a.repo_name = req.repo_name;
|
||||||
}
|
}
|
||||||
if let Some(alts) = alternatives_considered {
|
if let Some(alts) = req.alternatives_considered {
|
||||||
a.alternatives_considered = alts;
|
a.alternatives_considered = alts;
|
||||||
}
|
}
|
||||||
if let Some(aff) = affected_components {
|
if let Some(aff) = req.affected_components {
|
||||||
a.affected_components = aff;
|
a.affected_components = aff;
|
||||||
}
|
}
|
||||||
if author.is_some() {
|
if req.author.is_some() {
|
||||||
a.author = author;
|
a.author = req.author;
|
||||||
}
|
}
|
||||||
if git_commit.is_some() {
|
if req.git_commit.is_some() {
|
||||||
a.git_commit = git_commit;
|
a.git_commit = req.git_commit;
|
||||||
}
|
}
|
||||||
if git_branch.is_some() {
|
if req.git_branch.is_some() {
|
||||||
a.git_branch = git_branch;
|
a.git_branch = req.git_branch;
|
||||||
}
|
}
|
||||||
if task_id.is_some() {
|
if req.task_id.is_some() {
|
||||||
a.task_id = task_id;
|
a.task_id = req.task_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
adrs_to_index.push(a.clone());
|
adrs_to_index.push(a.clone());
|
||||||
@@ -513,10 +548,9 @@ impl McpTool for DecisionsHandler {
|
|||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DecisionsTool::Query { query, namespace, limit, include_body } => {
|
DecisionAction::Query => {
|
||||||
let ns = namespace.clone().unwrap_or_else(crate::models::default_namespace);
|
let limit = req.limit.unwrap_or(20);
|
||||||
let limit = limit.unwrap_or(20);
|
let include_body = req.include_body.unwrap_or(true);
|
||||||
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()
|
||||||
@@ -524,7 +558,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) = &query {
|
if let Some(q) = &req.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,
|
||||||
@@ -567,8 +601,10 @@ impl McpTool for DecisionsHandler {
|
|||||||
})?;
|
})?;
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
DecisionsTool::Delete { id } => {
|
DecisionAction::Delete => {
|
||||||
let id = id;
|
let id = req.id.ok_or_else(|| {
|
||||||
|
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) {
|
||||||
@@ -608,11 +644,15 @@ 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())?;
|
||||||
match req {
|
let ns = req
|
||||||
TechDebtTool::Log { description, title: _, ideal_solution, git_commit, git_branch, symbol_references: _, line_range, namespace, severity, repo_name, file_path, workaround, effort_estimate } => {
|
.namespace
|
||||||
let ns = namespace.clone().unwrap_or_else(crate::models::default_namespace);
|
.unwrap_or_else(crate::models::default_namespace);
|
||||||
let desc = description;
|
match req.action {
|
||||||
let ideal = ideal_solution.unwrap_or_default();
|
TechDebtAction::Log => {
|
||||||
|
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
|
||||||
@@ -625,15 +665,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: git_commit,
|
git_commit: req.git_commit,
|
||||||
git_branch: git_branch,
|
git_branch: req.git_branch,
|
||||||
embedding,
|
embedding,
|
||||||
repo_name: repo_name,
|
repo_name: req.repo_name,
|
||||||
severity: severity,
|
severity: req.severity,
|
||||||
file_path: file_path,
|
file_path: req.file_path,
|
||||||
line_range: line_range,
|
line_range: req.line_range,
|
||||||
workaround: workaround,
|
workaround: req.workaround,
|
||||||
effort_estimate: effort_estimate,
|
effort_estimate: req.effort_estimate,
|
||||||
});
|
});
|
||||||
if debts.len() > 300 {
|
if debts.len() > 300 {
|
||||||
let severity_rank =
|
let severity_rank =
|
||||||
@@ -657,8 +697,10 @@ impl McpTool for TechDebtHandler {
|
|||||||
});
|
});
|
||||||
Ok("Tech debt logged".to_string())
|
Ok("Tech debt logged".to_string())
|
||||||
}
|
}
|
||||||
TechDebtTool::Resolve { id } => {
|
TechDebtAction::Resolve => {
|
||||||
let id = id;
|
let id = req.id.ok_or_else(|| {
|
||||||
|
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() {
|
||||||
@@ -678,10 +720,9 @@ impl McpTool for TechDebtHandler {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TechDebtTool::List { include_resolved, namespace, summary_level, max_tokens } => {
|
TechDebtAction::List => {
|
||||||
let ns = namespace.clone().unwrap_or_else(crate::models::default_namespace);
|
let inc = req.include_resolved.unwrap_or(false);
|
||||||
let inc = include_resolved.unwrap_or(false);
|
let level = req.summary_level.as_deref().unwrap_or("detailed");
|
||||||
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()
|
||||||
@@ -702,7 +743,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) = max_tokens {
|
if let Some(max_t) = req.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);
|
||||||
@@ -1158,9 +1199,13 @@ 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 {
|
match req.action {
|
||||||
ManageCheckpointTool::Create { name_or_id, description, namespace } => {
|
CheckpointAction::Create => {
|
||||||
let name = name_or_id.ok_or_else(|| crate::error::AppError::Internal("name_or_id is required".to_string()))?;
|
let name = req.name_or_id.ok_or_else(|| {
|
||||||
|
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) {
|
||||||
@@ -1185,12 +1230,15 @@ 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) = &description {
|
if let Some(desc) = &req.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 = namespace.clone().unwrap_or_else(|| "global".to_string());
|
let ns = req
|
||||||
|
.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(),
|
||||||
@@ -1203,8 +1251,12 @@ impl McpTool for ManageCheckpointHandler {
|
|||||||
|
|
||||||
Ok(format!("Checkpoint '{}' created successfully.", name))
|
Ok(format!("Checkpoint '{}' created successfully.", name))
|
||||||
}
|
}
|
||||||
ManageCheckpointTool::Restore { name_or_id, namespace: _ } => {
|
CheckpointAction::Restore => {
|
||||||
let name = name_or_id.ok_or_else(|| crate::error::AppError::Internal("name_or_id is required".to_string()))?;
|
let name = req.name_or_id.ok_or_else(|| {
|
||||||
|
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() {
|
||||||
@@ -1245,7 +1297,7 @@ impl McpTool for ManageCheckpointHandler {
|
|||||||
|
|
||||||
Ok(format!("Checkpoint '{}' restored successfully.", name))
|
Ok(format!("Checkpoint '{}' restored successfully.", name))
|
||||||
}
|
}
|
||||||
ManageCheckpointTool::List { namespace: _ } => {
|
CheckpointAction::List => {
|
||||||
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) {
|
||||||
@@ -1263,8 +1315,12 @@ impl McpTool for ManageCheckpointHandler {
|
|||||||
}
|
}
|
||||||
Ok(serde_yaml::to_string(&list)?)
|
Ok(serde_yaml::to_string(&list)?)
|
||||||
}
|
}
|
||||||
ManageCheckpointTool::Delete { name_or_id, namespace: _ } => {
|
CheckpointAction::Delete => {
|
||||||
let name = name_or_id.ok_or_else(|| crate::error::AppError::Internal("name_or_id is required".to_string()))?;
|
let name = req.name_or_id.ok_or_else(|| {
|
||||||
|
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);
|
||||||
@@ -1460,9 +1516,14 @@ 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 {
|
|
||||||
HypothesesTool::Log { hypothesis, task_id, status, evidence } => {
|
match req.action {
|
||||||
let hyp_text = hypothesis;
|
HypothesisAction::Log => {
|
||||||
|
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()
|
||||||
@@ -1471,10 +1532,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: task_id,
|
task_id: req.task_id,
|
||||||
hypothesis: hyp_text,
|
hypothesis: hyp_text,
|
||||||
status: status.unwrap_or_else(|| "unverified".to_string()),
|
status: req.status.unwrap_or_else(|| "unverified".to_string()),
|
||||||
evidence: evidence,
|
evidence: req.evidence,
|
||||||
timestamp,
|
timestamp,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
@@ -1483,17 +1544,17 @@ impl McpTool for HypothesesHandler {
|
|||||||
|
|
||||||
Ok(format!("Hypothesis '{}' logged successfully.", hyp_id))
|
Ok(format!("Hypothesis '{}' logged successfully.", hyp_id))
|
||||||
}
|
}
|
||||||
HypothesesTool::Query { query, task_id, status: _ } => {
|
HypothesisAction::Query => {
|
||||||
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) = &task_id
|
if let Some(tid) = &req.task_id
|
||||||
&& h.task_id.as_ref() != Some(tid)
|
&& h.task_id.as_ref() != Some(tid)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if let Some(q) = &query {
|
if let Some(q) = &req.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
|
||||||
@@ -1604,11 +1665,24 @@ 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 {
|
|
||||||
AgentSignalsTool::Broadcast { sender, signal_type, payload, ttl_seconds } => {
|
match req.action {
|
||||||
let sender = sender;
|
AgentSignalAction::Broadcast => {
|
||||||
let signal_type = signal_type;
|
let sender = req.sender.ok_or_else(|| {
|
||||||
let payload = payload;
|
crate::error::AppError::Internal(
|
||||||
|
"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)
|
||||||
@@ -1622,7 +1696,7 @@ impl McpTool for AgentSignalsHandler {
|
|||||||
signal_type: signal_type.clone(),
|
signal_type: signal_type.clone(),
|
||||||
payload,
|
payload,
|
||||||
timestamp,
|
timestamp,
|
||||||
ttl_seconds: ttl_seconds,
|
ttl_seconds: req.ttl_seconds,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1650,7 +1724,7 @@ impl McpTool for AgentSignalsHandler {
|
|||||||
sig_id, sender
|
sig_id, sender
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
AgentSignalsTool::Query { sender, signal_type, limit } => {
|
AgentSignalAction::Query => {
|
||||||
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()
|
||||||
@@ -1665,19 +1739,19 @@ impl McpTool for AgentSignalsHandler {
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if let Some(sender) = &sender
|
if let Some(sender) = &req.sender
|
||||||
&& s.sender.to_lowercase() != sender.to_lowercase()
|
&& s.sender.to_lowercase() != sender.to_lowercase()
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if let Some(st) = &signal_type
|
if let Some(st) = &req.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(limit.unwrap_or(20))
|
.take(req.limit.unwrap_or(20))
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
});
|
});
|
||||||
@@ -1826,7 +1900,7 @@ mod tests {
|
|||||||
"title": "Architecture",
|
"title": "Architecture",
|
||||||
"context": "Needs DB",
|
"context": "Needs DB",
|
||||||
"decision": "Use SQLite",
|
"decision": "Use SQLite",
|
||||||
"consequences": "Simple",
|
"consequence": "Simple",
|
||||||
});
|
});
|
||||||
let res1 = decision_handler
|
let res1 = decision_handler
|
||||||
.execute(args_dec, state.clone())
|
.execute(args_dec, state.clone())
|
||||||
@@ -2038,7 +2112,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",
|
||||||
"consequences": "Fast async API routing"
|
"consequence": "Fast async API routing"
|
||||||
}),
|
}),
|
||||||
state.clone(),
|
state.clone(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,12 +23,15 @@ 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 {
|
match req.action {
|
||||||
TasksTool::Add { title, description, parent_id, dependencies, git_branch, namespace, repo_name, priority, assigned_agent, verification_command } => {
|
TaskAction::Add => {
|
||||||
let description = description.unwrap_or_default();
|
let title = req.title.ok_or_else(|| {
|
||||||
|
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 = dependencies.unwrap_or_default();
|
let deps = req.dependencies.unwrap_or_default();
|
||||||
|
|
||||||
let task = Task {
|
let task = Task {
|
||||||
id: task_id.clone(),
|
id: task_id.clone(),
|
||||||
@@ -37,16 +40,16 @@ impl McpTool for TasksHandler {
|
|||||||
description,
|
description,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
git_branch,
|
git_branch: req.git_branch,
|
||||||
parent_id,
|
parent_id: req.parent_id,
|
||||||
dependencies: deps,
|
dependencies: deps,
|
||||||
acceptance_criteria: vec![],
|
acceptance_criteria: vec![],
|
||||||
expires_at: None,
|
expires_at: None,
|
||||||
namespace: namespace.unwrap_or_else(|| "default".to_string()),
|
namespace: req.namespace.unwrap_or_else(|| "default".to_string()),
|
||||||
repo_name,
|
repo_name: req.repo_name,
|
||||||
priority,
|
priority: req.priority,
|
||||||
assigned_agent,
|
assigned_agent: req.assigned_agent,
|
||||||
verification_command,
|
verification_command: req.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));
|
||||||
@@ -72,7 +75,13 @@ impl McpTool for TasksHandler {
|
|||||||
});
|
});
|
||||||
Ok(format!("Task added with ID: {}", task_id))
|
Ok(format!("Task added with ID: {}", task_id))
|
||||||
}
|
}
|
||||||
TasksTool::Update { id, status } => {
|
TaskAction::Update => {
|
||||||
|
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;
|
||||||
@@ -193,7 +202,10 @@ impl McpTool for TasksHandler {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TasksTool::Delete { id } => {
|
TaskAction::Delete => {
|
||||||
|
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| {
|
||||||
@@ -255,19 +267,19 @@ impl McpTool for TasksHandler {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TasksTool::List { git_branch, status, summary_level, max_tokens, offset } => {
|
TaskAction::List => {
|
||||||
let level = summary_level.as_deref().unwrap_or("detailed");
|
let level = req.summary_level.as_deref().unwrap_or("detailed");
|
||||||
let offset = offset.unwrap_or(0);
|
let offset = req.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 status.as_deref() {
|
let status_match = match req.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 &git_branch {
|
let branch_match = match &req.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,
|
||||||
};
|
};
|
||||||
@@ -285,7 +297,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) = max_tokens {
|
if let Some(max_t) = req.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();
|
||||||
@@ -296,7 +308,13 @@ impl McpTool for TasksHandler {
|
|||||||
})?;
|
})?;
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
TasksTool::SetCriteria { id, criteria: criteria_list } => {
|
TaskAction::SetCriteria => {
|
||||||
|
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| {
|
||||||
@@ -337,8 +355,11 @@ impl McpTool for TasksHandler {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TasksTool::Verify { id, proof } => {
|
TaskAction::Verify => {
|
||||||
let proof_str = proof.unwrap_or_else(|| "Verified".to_string());
|
let id = req.id.ok_or_else(|| {
|
||||||
|
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;
|
||||||
@@ -415,29 +436,41 @@ 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 {
|
match req.action {
|
||||||
MilestonesTool::Add { title, namespace, target_date, description, deliverables, task_ids, repo_name } => {
|
MilestoneAction::Add => {
|
||||||
let ns = namespace.unwrap_or_else(crate::models::default_namespace);
|
let title = req.title.ok_or_else(|| {
|
||||||
|
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: target_date
|
target_date: req
|
||||||
|
.target_date
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.and_then(|s| s.parse::<u64>().ok()),
|
.and_then(|s| s.parse::<u64>().ok()),
|
||||||
description: description,
|
description: req.description,
|
||||||
task_ids: task_ids.unwrap_or_default(),
|
task_ids: req.task_ids.unwrap_or_default(),
|
||||||
deliverables: deliverables.unwrap_or_default(),
|
deliverables: req.deliverables.unwrap_or_default(),
|
||||||
repo_name,
|
repo_name: req.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())
|
||||||
}
|
}
|
||||||
MilestonesTool::Update { id, status, deliverables, task_ids } => {
|
MilestoneAction::Update => {
|
||||||
|
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() {
|
||||||
@@ -449,9 +482,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) = &task_ids {
|
if let Some(task_ids) = &req.task_ids {
|
||||||
m.task_ids = task_ids.clone();
|
m.task_ids = task_ids.clone();
|
||||||
if let Some(deliverables) = &deliverables {
|
if let Some(deliverables) = &req.deliverables {
|
||||||
m.deliverables = deliverables.clone();
|
m.deliverables = deliverables.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -469,12 +502,12 @@ impl McpTool for MilestonesHandler {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MilestonesTool::List { namespace } => {
|
MilestoneAction::List => {
|
||||||
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) = &namespace {
|
if let Some(ns) = &req.namespace {
|
||||||
&i.namespace == ns
|
&i.namespace == ns
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
|
|||||||
+41
-26
@@ -285,45 +285,58 @@ pub async fn memory_consolidation_worker(state: Arc<MemoryState>) {
|
|||||||
_ = interval.tick() => {},
|
_ = interval.tick() => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
let entities: Vec<String> = state.graph.read_with(|g| {
|
let entities: Vec<_> = state.graph.read_with(|g| {
|
||||||
g.entities.keys().cloned().collect()
|
g.entities
|
||||||
|
.values()
|
||||||
|
.map(|e| (e.name.clone(), e.entity_type.clone()))
|
||||||
|
.collect()
|
||||||
});
|
});
|
||||||
|
|
||||||
if entities.len() < 2 {
|
if entities.len() < 2 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut duplicates = None;
|
let mut entity_summaries = String::new();
|
||||||
let mut seen = std::collections::HashMap::new();
|
for (name, e_type) in entities.iter().take(50) {
|
||||||
for name in &entities {
|
entity_summaries.push_str(&format!("- [{}] {}\n", e_type, name));
|
||||||
let normalized: String = name
|
|
||||||
.chars()
|
|
||||||
.filter(|c| c.is_alphanumeric())
|
|
||||||
.flat_map(|c| c.to_lowercase())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Skip empty normalized names
|
|
||||||
if normalized.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(existing) = seen.insert(normalized, name.clone()) {
|
let prompt = format!(
|
||||||
if existing != *name {
|
"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{}",
|
||||||
duplicates = Some((existing, name.clone()));
|
entity_summaries
|
||||||
break;
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((e1_name, e2_name)) = duplicates {
|
if let Ok(response) = state
|
||||||
|
.ollama
|
||||||
|
.generate(
|
||||||
|
&prompt,
|
||||||
|
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!(
|
tracing::info!(
|
||||||
"Memory Consolidation Daemon: Merging '{}' into '{}'",
|
"Memory Consolidation Daemon: Merging '{}' into '{}'",
|
||||||
e2_name,
|
e2_name,
|
||||||
e1_name
|
e1_name
|
||||||
);
|
);
|
||||||
state.modify_graph(|g| {
|
state.modify_graph(|g| {
|
||||||
if let Some(mut e2) = g.entities.remove(&e2_name) {
|
if let Some(mut e2) = g.entities.remove(e2_name) {
|
||||||
if let Some(e1) = g.entities.get_mut(&e1_name) {
|
if let Some(e1) = g.entities.get_mut(e1_name) {
|
||||||
e1.observations.append(&mut e2.observations);
|
e1.observations.append(&mut e2.observations);
|
||||||
} else {
|
} else {
|
||||||
g.entities.insert(e2_name.clone(), e2);
|
g.entities.insert(e2_name.clone(), e2);
|
||||||
@@ -331,10 +344,10 @@ pub async fn memory_consolidation_worker(state: Arc<MemoryState>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for rel in g.relations.iter_mut() {
|
for rel in g.relations.iter_mut() {
|
||||||
if rel.from == e2_name {
|
if rel.from == *e2_name {
|
||||||
rel.from = e1_name.clone();
|
rel.from = e1_name.clone();
|
||||||
}
|
}
|
||||||
if rel.to == e2_name {
|
if rel.to == *e2_name {
|
||||||
rel.to = e1_name.clone();
|
rel.to = e1_name.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -342,6 +355,8 @@ pub async fn memory_consolidation_worker(state: Arc<MemoryState>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::Error>> {
|
pub async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let state_for_index = Arc::clone(&state);
|
let state_for_index = Arc::clone(&state);
|
||||||
|
|||||||
+1
-9
@@ -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 mut schema_val = serde_json::to_value(&schema).unwrap_or_default();
|
let schema_val = serde_json::to_value(&schema).unwrap_or_default();
|
||||||
|
|
||||||
let desc = schema_val
|
let desc = schema_val
|
||||||
.get("description")
|
.get("description")
|
||||||
@@ -31,14 +31,6 @@ 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,
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ 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>,
|
||||||
@@ -206,8 +207,11 @@ 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,
|
||||||
@@ -359,6 +363,7 @@ 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,
|
||||||
|
|||||||
@@ -479,7 +479,6 @@ 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);
|
||||||
|
|||||||
+366
-358
@@ -2,60 +2,23 @@ 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<EntityInput>,
|
pub entities: Vec<crate::models::Entity>,
|
||||||
}
|
|
||||||
|
|
||||||
#[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.
|
/// Array of relations to create. Each relation item MUST use 'from', 'to', and 'relation_type'.
|
||||||
pub relations: Vec<RelationInput>,
|
pub relations: Vec<crate::models::Relation>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,9 +38,7 @@ 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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +53,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<RelationInput>,
|
pub relations: Vec<crate::models::Relation>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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.
|
||||||
@@ -221,38 +182,26 @@ 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)]
|
||||||
#[serde(tag = "action", rename_all = "snake_case")]
|
pub struct ManageCheckpointTool {
|
||||||
pub enum ManageCheckpointTool {
|
/// Action to perform: 'create', 'restore', 'list', or 'delete'.
|
||||||
/// Save a checkpoint.
|
pub action: CheckpointAction,
|
||||||
Create {
|
|
||||||
/// Unique name or ID for the snapshot checkpoint.
|
/// Unique name or ID for the snapshot checkpoint.
|
||||||
name_or_id: Option<String>,
|
pub name_or_id: Option<String>,
|
||||||
/// Optional description of why this checkpoint was created.
|
/// Optional description of why this checkpoint was created.
|
||||||
description: Option<String>,
|
pub description: Option<String>,
|
||||||
/// Optional namespace filter.
|
/// Optional namespace filter.
|
||||||
namespace: Option<String>,
|
pub 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.
|
||||||
@@ -364,19 +313,14 @@ 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,30 +351,28 @@ 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)]
|
||||||
#[serde(tag = "action", rename_all = "snake_case")]
|
pub struct HypothesesTool {
|
||||||
pub enum HypothesesTool {
|
/// Action to perform: 'log' or 'query'.
|
||||||
/// Log a new diagnostic hypothesis.
|
pub action: HypothesisAction,
|
||||||
Log {
|
/// Optional task ID associated with this hypothesis or to filter hypotheses.
|
||||||
/// The diagnostic hypothesis or potential root cause.
|
pub task_id: Option<String>,
|
||||||
hypothesis: String,
|
/// The diagnostic hypothesis or potential root cause (required for action 'log').
|
||||||
/// Optional task ID associated with this hypothesis.
|
pub hypothesis: Option<String>,
|
||||||
task_id: Option<String>,
|
|
||||||
/// Status: 'unverified', 'verified', or 'rejected'. Defaults to 'unverified'.
|
/// Status: 'unverified', 'verified', or 'rejected'. Defaults to 'unverified'.
|
||||||
status: Option<String>,
|
pub status: Option<String>,
|
||||||
/// Evidence or test results supporting or disproving the hypothesis.
|
/// Evidence or test results supporting or disproving the hypothesis.
|
||||||
evidence: Option<String>,
|
pub evidence: Option<String>,
|
||||||
},
|
/// Optional search query text (for action 'query').
|
||||||
/// Query existing hypotheses.
|
pub query: Option<String>,
|
||||||
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 {
|
||||||
@@ -492,35 +434,27 @@ 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,
|
||||||
/// The AST node type to replace (e.g., 'function_item', 'impl_item').
|
pub node_type: String, // e.g., "function_item", "impl_item"
|
||||||
pub node_type: String,
|
pub node_name: String, // e.g., "execute"
|
||||||
/// 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)]
|
||||||
@@ -626,30 +560,28 @@ 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)]
|
||||||
#[serde(tag = "action", rename_all = "snake_case")]
|
pub struct AgentSignalsTool {
|
||||||
pub enum AgentSignalsTool {
|
/// Action to perform: 'broadcast' or 'query'.
|
||||||
/// Broadcast a signal to peer subagents.
|
pub action: AgentSignalAction,
|
||||||
Broadcast {
|
/// Sender agent ID or role (e.g. 'PrePushAuditor', 'MemoryLibrarian'). Required for broadcast; optional filter for query.
|
||||||
/// Sender agent ID or role (e.g. 'PrePushAuditor', 'MemoryLibrarian').
|
pub sender: Option<String>,
|
||||||
sender: String,
|
/// Signal type or event category (e.g. 'AUDIT_PASSED', 'REPRODUCER_READY', 'TESTS_FAILED'). Required for broadcast; optional filter for query.
|
||||||
/// Signal type or event category (e.g. 'AUDIT_PASSED', 'REPRODUCER_READY', 'TESTS_FAILED').
|
pub signal_type: Option<String>,
|
||||||
signal_type: String,
|
/// JSON or text payload containing event details or artifact URIs (required for action 'broadcast').
|
||||||
/// JSON or text payload containing event details or artifact URIs.
|
pub payload: Option<String>,
|
||||||
payload: String,
|
|
||||||
/// Optional Time-To-Live in seconds for the signal. Defaults to 3600 (1 hour).
|
/// Optional Time-To-Live in seconds for the signal. Defaults to 3600 (1 hour).
|
||||||
ttl_seconds: Option<u64>,
|
pub ttl_seconds: Option<u64>,
|
||||||
},
|
/// Optional limit on returned signals (for action 'query'). Defaults to 20.
|
||||||
/// Query active signals.
|
pub limit: Option<usize>,
|
||||||
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)]
|
||||||
@@ -665,12 +597,21 @@ 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'.
|
||||||
@@ -683,124 +624,150 @@ pub struct HandoffMemosTool {
|
|||||||
pub ids: Option<Vec<String>>,
|
pub ids: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Action-oriented task tracking system.
|
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[serde(rename_all = "snake_case")]
|
||||||
#[serde(tag = "action", rename_all = "snake_case")]
|
pub enum TaskAction {
|
||||||
pub enum TasksTool {
|
#[serde(alias = "add", alias = "ADD", alias = "Add")]
|
||||||
/// Create a new task.
|
Add,
|
||||||
Add {
|
#[serde(alias = "update", alias = "UPDATE", alias = "Update")]
|
||||||
/// Task title.
|
Update,
|
||||||
title: String,
|
#[serde(alias = "delete", alias = "DELETE", alias = "Delete")]
|
||||||
/// Task description.
|
Delete,
|
||||||
description: Option<String>,
|
#[serde(alias = "list", alias = "LIST", alias = "List")]
|
||||||
/// Optional parent task ID for subtasks.
|
List,
|
||||||
parent_id: Option<String>,
|
#[serde(
|
||||||
/// List of dependent task IDs.
|
alias = "set_criteria",
|
||||||
dependencies: Option<Vec<String>>,
|
alias = "setCriteria",
|
||||||
/// Branch associated with this task.
|
alias = "SET_CRITERIA",
|
||||||
git_branch: Option<String>,
|
alias = "SetCriteria"
|
||||||
/// Optional namespace filter or assignment.
|
)]
|
||||||
namespace: Option<String>,
|
SetCriteria,
|
||||||
/// Repository name.
|
#[serde(alias = "verify", alias = "VERIFY", alias = "Verify")]
|
||||||
repo_name: Option<String>,
|
Verify,
|
||||||
/// Priority: 'low', 'medium', 'high', 'critical'.
|
|
||||||
priority: Option<String>,
|
|
||||||
/// Assigned agent or subagent.
|
|
||||||
assigned_agent: Option<String>,
|
|
||||||
/// Command used to verify completion.
|
|
||||||
verification_command: Option<String>,
|
|
||||||
},
|
|
||||||
/// Update task status.
|
|
||||||
Update {
|
|
||||||
/// Task ID.
|
|
||||||
id: String,
|
|
||||||
/// New status: 'pending', 'completed', or 'cancelled'.
|
|
||||||
status: String,
|
|
||||||
},
|
|
||||||
/// Delete a task and its subtasks.
|
|
||||||
Delete {
|
|
||||||
/// Task ID.
|
|
||||||
id: String,
|
|
||||||
},
|
|
||||||
/// List active tasks.
|
|
||||||
List {
|
|
||||||
/// Optional Git branch filter.
|
|
||||||
git_branch: Option<String>,
|
|
||||||
/// Optional status filter.
|
|
||||||
status: 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>,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Project milestone management.
|
/// 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)]
|
||||||
#[serde(tag = "action", rename_all = "snake_case")]
|
pub struct TasksTool {
|
||||||
pub enum MilestonesTool {
|
/// Action to perform: 'add', 'update', 'delete', 'list', 'set_criteria', or 'verify'.
|
||||||
/// Create a new milestone.
|
pub action: TaskAction,
|
||||||
Add {
|
/// Task ID (required for 'update', 'delete', 'set_criteria', 'verify').
|
||||||
/// Milestone title.
|
#[serde(alias = "task_id", alias = "taskId")]
|
||||||
title: String,
|
pub id: Option<String>,
|
||||||
/// Optional namespace filter or assignment.
|
/// Task title (required for 'add').
|
||||||
namespace: Option<String>,
|
#[serde(alias = "name")]
|
||||||
/// Optional target completion date.
|
pub title: Option<String>,
|
||||||
target_date: Option<String>,
|
/// Task description (optional for 'add').
|
||||||
/// Optional description of milestone scope.
|
#[serde(alias = "desc")]
|
||||||
description: Option<String>,
|
pub description: Option<String>,
|
||||||
/// Optional deliverables or task checklist.
|
/// New status: 'pending', 'completed', or 'cancelled' (for 'update').
|
||||||
deliverables: Option<Vec<String>>,
|
pub status: Option<String>,
|
||||||
/// Optional list of task IDs associated with this milestone.
|
/// Parent task ID (optional for 'add').
|
||||||
#[serde(default)]
|
#[serde(alias = "parentId", alias = "parent")]
|
||||||
task_ids: Option<Vec<String>>,
|
pub parent_id: Option<String>,
|
||||||
|
/// List of dependency task IDs (optional for 'add').
|
||||||
|
#[serde(alias = "deps")]
|
||||||
|
pub dependencies: Option<Vec<String>>,
|
||||||
|
/// Git branch filter or assignment.
|
||||||
|
#[serde(alias = "branch")]
|
||||||
|
pub git_branch: Option<String>,
|
||||||
|
/// Acceptance criteria (required for 'set_criteria').
|
||||||
|
#[serde(alias = "acceptance_criteria", alias = "acceptanceCriteria")]
|
||||||
|
pub criteria: Option<Vec<String>>,
|
||||||
|
/// Verification proof or details (optional for 'verify').
|
||||||
|
pub proof: Option<String>,
|
||||||
|
/// Summary level: 'compact', 'detailed', or 'full' (for 'list').
|
||||||
|
pub summary_level: Option<String>,
|
||||||
|
/// Maximum tokens budget cap (for 'list').
|
||||||
|
pub max_tokens: Option<usize>,
|
||||||
|
/// Optional namespace filter or assignment (defaults to 'default').
|
||||||
|
pub namespace: Option<String>,
|
||||||
/// Optional repository name.
|
/// Optional repository name.
|
||||||
repo_name: Option<String>,
|
pub repo_name: Option<String>,
|
||||||
},
|
/// Optional task priority: 'low', 'medium', 'high', 'urgent'.
|
||||||
/// Update milestone status.
|
pub priority: Option<String>,
|
||||||
Update {
|
/// Optional assigned subagent role or identifier.
|
||||||
/// Milestone ID.
|
pub assigned_agent: Option<String>,
|
||||||
id: String,
|
/// Optional verification command to validate criteria completion.
|
||||||
/// New status.
|
/// Optional pagination offset for 'list'.
|
||||||
status: String,
|
pub offset: Option<usize>,
|
||||||
|
pub verification_command: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||||
|
#[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)]
|
||||||
|
pub struct MilestonesTool {
|
||||||
|
/// Action to perform: 'add', 'update', or 'list'.
|
||||||
|
pub action: MilestoneAction,
|
||||||
|
/// Milestone ID (required for 'update').
|
||||||
|
#[serde(alias = "milestone_id", alias = "milestoneId")]
|
||||||
|
pub id: Option<String>,
|
||||||
|
/// Milestone title (required for 'add').
|
||||||
|
#[serde(alias = "name")]
|
||||||
|
pub title: Option<String>,
|
||||||
|
/// New status (for 'update').
|
||||||
|
pub status: Option<String>,
|
||||||
|
/// Optional namespace filter or assignment.
|
||||||
|
pub namespace: Option<String>,
|
||||||
|
/// Optional target completion date.
|
||||||
|
pub target_date: Option<String>,
|
||||||
|
/// Optional description of milestone scope.
|
||||||
|
pub description: Option<String>,
|
||||||
/// Optional deliverables or task checklist.
|
/// Optional deliverables or task checklist.
|
||||||
deliverables: Option<Vec<String>>,
|
pub deliverables: Option<Vec<String>>,
|
||||||
/// Optional list of task IDs associated with this milestone.
|
/// Optional list of task IDs associated with this milestone.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
task_ids: Option<Vec<String>>,
|
pub task_ids: Option<Vec<String>>,
|
||||||
},
|
/// Optional repository name.
|
||||||
/// List milestones.
|
pub repo_name: Option<String>,
|
||||||
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'.
|
||||||
@@ -831,154 +798,177 @@ pub struct SnippetsTool {
|
|||||||
pub line_range: Option<String>,
|
pub line_range: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Architectural Decision Records (ADRs) log.
|
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq, Clone)]
|
||||||
#[derive(Deserialize, Serialize, JsonSchema, Debug, Clone)]
|
#[serde(rename_all = "snake_case")]
|
||||||
#[serde(tag = "action", rename_all = "snake_case")]
|
pub enum DecisionAction {
|
||||||
pub enum DecisionsTool {
|
#[serde(alias = "log", alias = "LOG", alias = "Log")]
|
||||||
/// Record an ADR.
|
Log,
|
||||||
Log {
|
#[serde(alias = "query", alias = "QUERY", alias = "Query")]
|
||||||
/// ADR title.
|
Query,
|
||||||
title: String,
|
#[serde(alias = "delete", alias = "DELETE", alias = "Delete")]
|
||||||
/// ADR status.
|
Delete,
|
||||||
status: Option<String>,
|
#[serde(alias = "update", alias = "UPDATE", alias = "Update")]
|
||||||
/// Context & problem statement.
|
Update,
|
||||||
context: Option<String>,
|
|
||||||
/// Decision made.
|
|
||||||
decision: Option<String>,
|
|
||||||
/// Consequences & tradeoffs.
|
|
||||||
consequences: 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>,
|
|
||||||
/// 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>,
|
|
||||||
},
|
|
||||||
/// Search ADRs.
|
|
||||||
Query {
|
|
||||||
/// Search query string.
|
|
||||||
query: Option<String>,
|
|
||||||
/// Optional namespace.
|
|
||||||
namespace: 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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Technical debt backlog management.
|
/// Architectural Decision Records (ADRs) log (log, query, update, delete).
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
/// Actions:
|
||||||
#[serde(tag = "action", rename_all = "snake_case")]
|
/// - 'log': Record ADR. Required: title. Optional: status, context, decision, consequences, namespace.
|
||||||
pub enum TechDebtTool {
|
/// - 'query': Search ADRs. Optional: query, namespace.
|
||||||
/// Record technical debt.
|
/// - 'update': Update ADR status or implementation info. Required: id. Optional: status, git_commit, git_branch, task_id, context, decision, consequences, title.
|
||||||
Log {
|
/// - 'delete': Remove ADR. Required: id.
|
||||||
/// Tech debt description.
|
///
|
||||||
description: String,
|
/// Next steps on error: Provide title for 'log' or id for 'update'/'delete'.
|
||||||
/// Optional title or summary of tech debt.
|
#[derive(Deserialize, Serialize, JsonSchema, Debug, Clone)]
|
||||||
title: Option<String>,
|
pub struct DecisionsTool {
|
||||||
/// Ideal solution.
|
/// Action to perform: 'log', 'query', or 'delete'.
|
||||||
ideal_solution: Option<String>,
|
pub action: DecisionAction,
|
||||||
/// Associated git commit hash.
|
/// ADR ID (required for 'delete').
|
||||||
git_commit: Option<String>,
|
pub id: Option<String>,
|
||||||
/// Associated git branch.
|
/// ADR title (required for 'log').
|
||||||
git_branch: Option<String>,
|
pub title: Option<String>,
|
||||||
/// Symbol references associated with the tech debt.
|
/// ADR status (for 'log').
|
||||||
symbol_references: Option<Vec<String>>,
|
pub status: Option<String>,
|
||||||
/// Line range associated with the tech debt.
|
/// Context & problem statement (for 'log').
|
||||||
line_range: Option<String>,
|
pub context: Option<String>,
|
||||||
|
/// Decision made (for 'log').
|
||||||
|
pub decision: Option<String>,
|
||||||
|
/// Consequences & tradeoffs (for 'log').
|
||||||
|
#[serde(alias = "consequence")]
|
||||||
|
pub consequences: Option<String>,
|
||||||
|
/// Search query string (for 'query').
|
||||||
|
pub query: Option<String>,
|
||||||
/// Optional namespace.
|
/// Optional namespace.
|
||||||
namespace: Option<String>,
|
pub namespace: Option<String>,
|
||||||
/// Optional debt severity: 'low', 'medium', 'high', 'critical'.
|
|
||||||
severity: Option<String>,
|
|
||||||
/// Optional repository name.
|
/// Optional repository name.
|
||||||
repo_name: Option<String>,
|
pub repo_name: Option<String>,
|
||||||
/// Optional file path where debt exists.
|
/// Optional author or architect behind the decision.
|
||||||
file_path: Option<String>,
|
pub author: Option<String>,
|
||||||
/// Optional workaround currently in place.
|
/// Optional affected system components or crates.
|
||||||
workaround: Option<String>,
|
pub affected_components: Option<Vec<String>>,
|
||||||
/// Optional effort estimate to fix.
|
/// Optional alternative designs or libraries considered.
|
||||||
effort_estimate: Option<String>,
|
pub alternatives_considered: Option<Vec<String>>,
|
||||||
},
|
/// Optional superseded ADR ID.
|
||||||
/// Mark tech debt resolved.
|
pub supersedes: Option<String>,
|
||||||
Resolve {
|
/// Limit the number of query results. Defaults to 20.
|
||||||
/// Tech debt ID.
|
pub limit: Option<usize>,
|
||||||
id: String,
|
/// Include the full body of the matched decisions. Defaults to true.
|
||||||
},
|
pub include_body: Option<bool>,
|
||||||
/// List tech debt items.
|
/// Optional Git commit where decision was implemented.
|
||||||
List {
|
pub git_commit: Option<String>,
|
||||||
/// Include resolved tech debt items.
|
/// Optional Git branch where decision was implemented.
|
||||||
include_resolved: Option<bool>,
|
pub git_branch: Option<String>,
|
||||||
/// Optional namespace.
|
/// Optional task ID linked to this decision.
|
||||||
namespace: Option<String>,
|
pub task_id: 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)]
|
||||||
|
#[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)]
|
||||||
|
pub struct TechDebtTool {
|
||||||
|
/// Action to perform: 'log', 'resolve', or 'list'.
|
||||||
|
pub action: TechDebtAction,
|
||||||
|
/// Tech debt ID (required for 'resolve').
|
||||||
|
#[serde(alias = "tech_debt_id", alias = "debt_id")]
|
||||||
|
pub id: Option<String>,
|
||||||
|
/// Tech debt description (required for 'log').
|
||||||
|
#[serde(alias = "desc")]
|
||||||
|
pub description: Option<String>,
|
||||||
|
/// Optional title or summary of tech debt.
|
||||||
|
pub title: Option<String>,
|
||||||
|
/// Ideal solution (optional for 'log').
|
||||||
|
#[serde(alias = "solution", alias = "fix")]
|
||||||
|
pub ideal_solution: Option<String>,
|
||||||
|
/// Include resolved tech debt items (for 'list').
|
||||||
|
pub include_resolved: Option<bool>,
|
||||||
|
/// Associated git commit hash.
|
||||||
|
#[serde(alias = "commit")]
|
||||||
|
pub git_commit: Option<String>,
|
||||||
|
/// Associated git branch.
|
||||||
|
#[serde(alias = "branch")]
|
||||||
|
pub git_branch: Option<String>,
|
||||||
|
/// Symbol references associated with the tech debt.
|
||||||
|
pub symbol_references: Option<Vec<String>>,
|
||||||
|
/// Line range associated with the tech debt.
|
||||||
|
pub line_range: Option<String>,
|
||||||
|
/// Optional namespace.
|
||||||
|
pub namespace: Option<String>,
|
||||||
|
/// Optional debt severity: 'low', 'medium', 'high', 'critical'.
|
||||||
|
pub severity: Option<String>,
|
||||||
|
/// Optional repository name.
|
||||||
|
pub repo_name: Option<String>,
|
||||||
|
/// Optional file path where debt exists.
|
||||||
|
pub file_path: Option<String>,
|
||||||
|
/// Optional workaround currently in place.
|
||||||
|
pub workaround: Option<String>,
|
||||||
|
/// Optional effort estimate to fix.
|
||||||
|
pub effort_estimate: Option<String>,
|
||||||
|
/// Summary level for list action: 'compact', 'detailed', or 'full'.
|
||||||
|
pub summary_level: Option<String>,
|
||||||
|
/// Optional token budget cap for list output.
|
||||||
|
pub 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'.
|
||||||
@@ -1008,19 +998,37 @@ 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', or 'write'.
|
/// Action to perform: 'image', 'text', 'history', 'clear', 'read', 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>,
|
||||||
|
|||||||
@@ -29,10 +29,11 @@ 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();
|
||||||
let parts: Vec<&str> = nvim_source.split("tool_def").collect();
|
for line in nvim_source.lines() {
|
||||||
for part in parts.iter().skip(1) {
|
if line.contains("\"name\": \"nvim_")
|
||||||
if let Some(start) = part.find("(\"") {
|
&& let Some(start) = line.find("\"name\": \"")
|
||||||
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());
|
||||||
}
|
}
|
||||||
|
|||||||
Whitespace-only changes.
Reference in new issue
Block a user