reasonkit-core 0.1.8

The Reasoning Engine — Auditable Reasoning for Production AI | Rust-Native | Turn Prompts into Protocols
//! Architect Agent MCP Handler
//!
//! Provides architectural guidance and ADR management tools.
//!
//! # Available Tools
//!
//! | Tool | Description |
//! |------|-------------|
//! | `architect_create_adr` | Generates a new ADR file |
//! | `architect_list_decisions` | Lists existing ADRs |
//! | `architect_review_design` | Validates design against principles |

use crate::error::{Error, Result};
use crate::mcp::tools::{Tool, ToolHandler, ToolResult};
use async_trait::async_trait;
use serde_json::{json, Value};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use tracing::{info, instrument};

/// Architect Agent MCP Handler
pub struct ArchitectAgentHandler;

impl Default for ArchitectAgentHandler {
    fn default() -> Self {
        Self::new()
    }
}

impl ArchitectAgentHandler {
    pub fn new() -> Self {
        Self
    }

    /// Get tool definitions
    pub fn tool_definitions() -> Vec<Tool> {
        vec![
            Self::create_adr_tool(),
            Self::list_decisions_tool(),
            Self::review_design_tool(),
        ]
    }

    fn create_adr_tool() -> Tool {
        Tool::with_schema(
            "architect_create_adr",
            "Creates a new Architecture Decision Record (ADR) file.",
            json!({
                "type": "object",
                "properties": {
                    "title": { "type": "string", "description": "Title of the decision" },
                    "status": { "type": "string", "description": "Status (Proposed, Accepted)", "default": "Proposed" },
                    "context": { "type": "string", "description": "Context and problem statement" },
                    "decision": { "type": "string", "description": "The decision made" },
                    "consequences": { "type": "string", "description": "Consequences (positive/negative)" },
                    "directory": { "type": "string", "description": "Output directory", "default": "docs/adr" }
                },
                "required": ["title", "context", "decision"],
                "additionalProperties": false
            }),
        )
    }

    fn list_decisions_tool() -> Tool {
        Tool::with_schema(
            "architect_list_decisions",
            "Lists all Architecture Decision Records (ADRs) in the project.",
            json!({
                "type": "object",
                "properties": {
                    "directory": { "type": "string", "description": "ADR directory", "default": "docs/adr" }
                },
                "additionalProperties": false
            }),
        )
    }

    fn review_design_tool() -> Tool {
        Tool::with_schema(
            "architect_review_design",
            "Reviews a design description against architectural principles.",
            json!({
                "type": "object",
                "properties": {
                    "content": { "type": "string", "description": "Design content or path to file" },
                    "focus": { "type": "string", "description": "Focus area (e.g., scalability)" }
                },
                "required": ["content"],
                "additionalProperties": false
            }),
        )
    }

    #[instrument(skip(self, arguments), fields(tool = %name))]
    pub async fn call_tool(
        &self,
        name: &str,
        arguments: HashMap<String, Value>,
    ) -> Result<ToolResult> {
        info!(tool = %name, "Executing Architect Agent Tool");

        match name {
            "architect_create_adr" => self.handle_create_adr(arguments).await,
            "architect_list_decisions" => self.handle_list_decisions(arguments).await,
            "architect_review_design" => self.handle_review_design(arguments).await,
            _ => Ok(ToolResult::error(format!("Unknown tool: {}", name))),
        }
    }

    async fn handle_create_adr(&self, args: HashMap<String, Value>) -> Result<ToolResult> {
        let title = args
            .get("title")
            .and_then(|v| v.as_str())
            .unwrap_or("Untitled");
        let status = args
            .get("status")
            .and_then(|v| v.as_str())
            .unwrap_or("Proposed");
        let context = args.get("context").and_then(|v| v.as_str()).unwrap_or("");
        let decision = args.get("decision").and_then(|v| v.as_str()).unwrap_or("");
        let consequences = args
            .get("consequences")
            .and_then(|v| v.as_str())
            .unwrap_or("");
        let dir = args
            .get("directory")
            .and_then(|v| v.as_str())
            .unwrap_or("docs/adr");

        // Simple slugification
        let slug = title.to_lowercase().replace(" ", "-");
        let date = chrono::Local::now().format("%Y-%m-%d").to_string();
        let filename = format!("{}-{}.md", date, slug); // Simplified naming for now
        let path = Path::new(dir).join(&filename);

        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)
                .map_err(|e| Error::Mcp(format!("Failed to create dir: {}", e)))?;
        }

        let content = format!(
            "# ADR: {}

**Status:** {}
**Date:** {}

## Context
{}

## Decision
{}

## Consequences
{}
",
            title, status, date, context, decision, consequences
        );

        fs::write(&path, &content)
            .map_err(|e| Error::Mcp(format!("Failed to write ADR: {}", e)))?;

        Ok(ToolResult::text(format!(
            "Created ADR at: {}",
            path.display()
        )))
    }

    async fn handle_list_decisions(&self, args: HashMap<String, Value>) -> Result<ToolResult> {
        let dir_str = args
            .get("directory")
            .and_then(|v| v.as_str())
            .unwrap_or("docs/adr");
        let dir = Path::new(dir_str);

        if !dir.exists() {
            return Ok(ToolResult::text("No ADR directory found."));
        }

        let mut adrs = Vec::new();
        if let Ok(entries) = fs::read_dir(dir) {
            for entry in entries.flatten() {
                let path = entry.path();
                if path.extension().is_some_and(|ext| ext == "md") {
                    let name = path.file_name().unwrap().to_string_lossy().to_string();
                    adrs.push(name);
                }
            }
        }
        adrs.sort();

        Ok(ToolResult::text(json!({ "decisions": adrs }).to_string()))
    }

    async fn handle_review_design(&self, args: HashMap<String, Value>) -> Result<ToolResult> {
        let content = args.get("content").and_then(|v| v.as_str()).unwrap_or("");

        // Mock review logic for now
        let mut findings = Vec::new();

        if !content.contains("trade-off") && !content.contains("tradeoff") {
            findings.push("Missing trade-off analysis.");
        }
        if content.len() < 100 {
            findings.push("Description too short for architectural review.");
        }

        let result = json!({
            "status": "reviewed",
            "findings": findings,
            "score": if findings.is_empty() { 90 } else { 70 }
        });

        Ok(ToolResult::text(serde_json::to_string_pretty(&result)?))
    }
}

#[async_trait]
impl ToolHandler for ArchitectAgentHandler {
    async fn call(&self, arguments: HashMap<String, Value>) -> Result<ToolResult> {
        let tool_name = arguments
            .get("_tool")
            .and_then(|v| v.as_str())
            .map(|s| s.to_string())
            .ok_or_else(|| Error::Mcp("Missing _tool identifier".into()))?;

        self.call_tool(&tool_name, arguments).await
    }
}