summaryrefslogtreecommitdiff
path: root/makima/src/llm
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/llm')
-rw-r--r--makima/src/llm/claude.rs304
-rw-r--r--makima/src/llm/contract_evaluator.rs97
-rw-r--r--makima/src/llm/contract_tools.rs1228
-rw-r--r--makima/src/llm/discuss_tools.rs210
-rw-r--r--makima/src/llm/groq.rs177
-rw-r--r--makima/src/llm/markdown.rs334
-rw-r--r--makima/src/llm/mesh_tools.rs1411
-rw-r--r--makima/src/llm/mod.rs73
-rw-r--r--makima/src/llm/phase_guidance.rs1032
-rw-r--r--makima/src/llm/task_output.rs485
-rw-r--r--makima/src/llm/templates.rs80
-rw-r--r--makima/src/llm/tools.rs1675
-rw-r--r--makima/src/llm/transcript_analyzer.rs292
13 files changed, 0 insertions, 7398 deletions
diff --git a/makima/src/llm/claude.rs b/makima/src/llm/claude.rs
deleted file mode 100644
index f475acd..0000000
--- a/makima/src/llm/claude.rs
+++ /dev/null
@@ -1,304 +0,0 @@
-//! Claude API client for LLM tool calling.
-
-use serde::{Deserialize, Serialize};
-use thiserror::Error;
-
-use super::tools::{Tool, ToolCall};
-
-const CLAUDE_API_URL: &str = "https://api.anthropic.com/v1/messages";
-const ANTHROPIC_VERSION: &str = "2023-06-01";
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum ClaudeModel {
- Opus,
- Sonnet,
-}
-
-impl ClaudeModel {
- pub fn model_id(&self) -> &'static str {
- match self {
- ClaudeModel::Opus => "claude-opus-4-5-20251101",
- ClaudeModel::Sonnet => "claude-sonnet-4-5-20250929",
- }
- }
-}
-
-impl Default for ClaudeModel {
- fn default() -> Self {
- ClaudeModel::Opus
- }
-}
-
-#[derive(Debug, Error)]
-pub enum ClaudeError {
- #[error("HTTP request failed: {0}")]
- Request(#[from] reqwest::Error),
- #[error("API error: {0}")]
- Api(String),
- #[error("Missing API key")]
- MissingApiKey,
-}
-
-#[derive(Debug, Clone)]
-pub struct ClaudeClient {
- api_key: String,
- client: reqwest::Client,
- model: ClaudeModel,
-}
-
-// Request types
-#[derive(Debug, Serialize)]
-struct ClaudeRequest {
- model: String,
- max_tokens: u32,
- messages: Vec<Message>,
- #[serde(skip_serializing_if = "Option::is_none")]
- tools: Option<Vec<ToolDefinition>>,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct Message {
- pub role: String,
- pub content: MessageContent,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(untagged)]
-pub enum MessageContent {
- Text(String),
- Blocks(Vec<ContentBlock>),
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(tag = "type")]
-pub enum ContentBlock {
- #[serde(rename = "text")]
- Text { text: String },
- #[serde(rename = "tool_use")]
- ToolUse {
- id: String,
- name: String,
- input: serde_json::Value,
- },
- #[serde(rename = "tool_result")]
- ToolResult {
- tool_use_id: String,
- content: String,
- },
-}
-
-#[derive(Debug, Serialize)]
-struct ToolDefinition {
- name: String,
- description: String,
- input_schema: serde_json::Value,
-}
-
-// Response types
-#[derive(Debug, Deserialize)]
-struct ClaudeResponse {
- content: Vec<ResponseContentBlock>,
- stop_reason: Option<String>,
-}
-
-#[derive(Debug, Clone, Deserialize)]
-#[serde(tag = "type")]
-pub enum ResponseContentBlock {
- #[serde(rename = "text")]
- Text { text: String },
- #[serde(rename = "tool_use")]
- ToolUse {
- id: String,
- name: String,
- input: serde_json::Value,
- },
-}
-
-#[derive(Debug)]
-pub struct ChatResult {
- pub content: Option<String>,
- pub tool_calls: Vec<ToolCall>,
- /// Raw tool use blocks for including in subsequent messages
- pub raw_tool_uses: Vec<ResponseContentBlock>,
- pub stop_reason: String,
-}
-
-impl ClaudeClient {
- pub fn new(api_key: String, model: ClaudeModel) -> Self {
- Self {
- api_key,
- client: reqwest::Client::new(),
- model,
- }
- }
-
- pub fn from_env(model: ClaudeModel) -> Result<Self, ClaudeError> {
- let api_key = std::env::var("ANTHROPIC_API_KEY").map_err(|_| ClaudeError::MissingApiKey)?;
- Ok(Self::new(api_key, model))
- }
-
- pub async fn chat_with_tools(
- &self,
- messages: Vec<Message>,
- tools: &[Tool],
- ) -> Result<ChatResult, ClaudeError> {
- let tool_definitions: Vec<ToolDefinition> = tools
- .iter()
- .map(|t| ToolDefinition {
- name: t.name.clone(),
- description: t.description.clone(),
- input_schema: t.parameters.clone(),
- })
- .collect();
-
- let request = ClaudeRequest {
- model: self.model.model_id().to_string(),
- max_tokens: 4096,
- messages,
- tools: Some(tool_definitions),
- };
-
- let response = self
- .client
- .post(CLAUDE_API_URL)
- .header("x-api-key", &self.api_key)
- .header("anthropic-version", ANTHROPIC_VERSION)
- .header("Content-Type", "application/json")
- .json(&request)
- .send()
- .await?;
-
- if !response.status().is_success() {
- let error_text = response.text().await.unwrap_or_default();
- return Err(ClaudeError::Api(error_text));
- }
-
- let claude_response: ClaudeResponse = response.json().await?;
-
- let stop_reason = claude_response.stop_reason.unwrap_or_else(|| "end_turn".to_string());
-
- // Extract text content and tool uses from content blocks
- let mut text_parts: Vec<String> = Vec::new();
- let mut raw_tool_uses: Vec<ResponseContentBlock> = Vec::new();
-
- for block in &claude_response.content {
- match block {
- ResponseContentBlock::Text { text } => {
- if !text.is_empty() {
- text_parts.push(text.clone());
- }
- }
- ResponseContentBlock::ToolUse { .. } => {
- raw_tool_uses.push(block.clone());
- }
- }
- }
-
- let content = if text_parts.is_empty() {
- None
- } else {
- Some(text_parts.join("\n"))
- };
-
- // Convert tool uses to ToolCalls
- let tool_calls: Vec<ToolCall> = raw_tool_uses
- .iter()
- .filter_map(|block| {
- if let ResponseContentBlock::ToolUse { id, name, input } = block {
- Some(ToolCall {
- id: id.clone(),
- name: name.clone(),
- arguments: input.clone(),
- })
- } else {
- None
- }
- })
- .collect();
-
- Ok(ChatResult {
- content,
- tool_calls,
- raw_tool_uses,
- stop_reason,
- })
- }
-}
-
-/// Helper to convert Groq-style messages to Claude messages
-pub fn groq_messages_to_claude(messages: &[super::groq::Message]) -> Vec<Message> {
- let mut claude_messages: Vec<Message> = Vec::new();
-
- for msg in messages {
- match msg.role.as_str() {
- "system" => {
- // Claude handles system prompts as first user message
- if let Some(ref content) = msg.content {
- claude_messages.push(Message {
- role: "user".to_string(),
- content: MessageContent::Text(format!("[System Instructions]: {}", content)),
- });
- // Add assistant acknowledgment to maintain conversation structure
- claude_messages.push(Message {
- role: "assistant".to_string(),
- content: MessageContent::Text("Understood. I'll follow these instructions.".to_string()),
- });
- }
- }
- "user" => {
- if let Some(ref content) = msg.content {
- claude_messages.push(Message {
- role: "user".to_string(),
- content: MessageContent::Text(content.clone()),
- });
- }
- }
- "assistant" => {
- let mut blocks: Vec<ContentBlock> = Vec::new();
-
- // Add text content if present
- if let Some(ref content) = msg.content {
- if !content.is_empty() {
- blocks.push(ContentBlock::Text { text: content.clone() });
- }
- }
-
- // Add tool uses if present
- if let Some(ref tool_calls) = msg.tool_calls {
- for tc in tool_calls {
- let input: serde_json::Value =
- serde_json::from_str(&tc.function.arguments).unwrap_or_default();
- blocks.push(ContentBlock::ToolUse {
- id: tc.id.clone(),
- name: tc.function.name.clone(),
- input,
- });
- }
- }
-
- if !blocks.is_empty() {
- claude_messages.push(Message {
- role: "assistant".to_string(),
- content: MessageContent::Blocks(blocks),
- });
- }
- }
- "tool" => {
- // Tool results in Claude go in a user message with tool_result blocks
- if let Some(ref content) = msg.content {
- let tool_use_id = msg.tool_call_id.clone().unwrap_or_default();
- claude_messages.push(Message {
- role: "user".to_string(),
- content: MessageContent::Blocks(vec![ContentBlock::ToolResult {
- tool_use_id,
- content: content.clone(),
- }]),
- });
- }
- }
- _ => {}
- }
- }
-
- claude_messages
-}
diff --git a/makima/src/llm/contract_evaluator.rs b/makima/src/llm/contract_evaluator.rs
deleted file mode 100644
index e63bbfa..0000000
--- a/makima/src/llm/contract_evaluator.rs
+++ /dev/null
@@ -1,97 +0,0 @@
-//! Contract Evaluator - LLM-based evaluation of completed contracts against directive.
-//!
-//! This module will be reimplemented as part of the directive verification engine.
-//! See the orchestration module for the new evaluation system.
-//!
-//! The new evaluation system will provide:
-//! - Tiered verification (programmatic verifiers first, then LLM evaluation)
-//! - Composite confidence scoring (weighted combination of results)
-//! - Pluggable verifier interface (test runner, linter, build, type checker)
-//! - Proper integration with the directive chain steps
-
-use serde::{Deserialize, Serialize};
-use sqlx::PgPool;
-use uuid::Uuid;
-
-// use crate::db::models::{Contract, DirectiveAcceptanceCriterion, DirectiveRequirement};
-
-/// Result of contract evaluation
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct ContractEvaluationResult {
- /// Whether the contract passed evaluation
- pub passed: bool,
- /// Overall score from 0.0 to 1.0
- pub overall_score: f64,
- /// Results for each acceptance criterion
- pub criteria_results: Vec<EvaluationCriterionResultLegacy>,
- /// Summary feedback from the evaluator
- pub summary_feedback: String,
- /// Instructions for rework if failed
- pub rework_instructions: Option<String>,
-}
-
-/// Per-criterion evaluation result (legacy - kept for compatibility)
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct EvaluationCriterionResultLegacy {
- pub criterion_id: String,
- pub criterion_text: String,
- pub passed: bool,
- /// Score (0.0-1.0)
- pub score: f64,
- pub feedback: String,
- /// Evidence supporting the evaluation
- pub evidence: Vec<String>,
-}
-
-/// File content for evaluation context
-#[derive(Debug, Clone)]
-pub struct FileContent {
- pub path: String,
- pub content: String,
-}
-
-/// Contract evaluator for LLM-based assessment.
-///
-/// NOTE: This is a stub implementation. The full evaluation system will be
-/// implemented as part of the orchestration/verifier module.
-pub struct ContractEvaluator {
- _pool: PgPool,
-}
-
-impl ContractEvaluator {
- /// Create a new contract evaluator.
- pub fn new(pool: PgPool) -> Self {
- Self { _pool: pool }
- }
-
- /// Evaluate a contract - stub implementation.
- ///
- /// This will be reimplemented in the orchestration module with:
- /// - Programmatic verification (tests, lint, build)
- /// - LLM evaluation
- /// - Composite scoring
- pub async fn evaluate_contract(
- &self,
- _contract_id: Uuid,
- ) -> Result<ContractEvaluationResult, ContractEvaluatorError> {
- // TODO: Implement using the new directive evaluation system
- Err(ContractEvaluatorError::NotImplemented(
- "Contract evaluator will be reimplemented with directive system".to_string(),
- ))
- }
-}
-
-/// Error types for contract evaluation.
-#[derive(Debug, thiserror::Error)]
-pub enum ContractEvaluatorError {
- #[error("Database error: {0}")]
- Database(#[from] sqlx::Error),
-
- #[error("LLM error: {0}")]
- Llm(String),
-
- #[error("Not implemented: {0}")]
- NotImplemented(String),
-}
diff --git a/makima/src/llm/contract_tools.rs b/makima/src/llm/contract_tools.rs
deleted file mode 100644
index 38d1a7e..0000000
--- a/makima/src/llm/contract_tools.rs
+++ /dev/null
@@ -1,1228 +0,0 @@
-//! Tool definitions for contract management via LLM.
-//!
-//! These tools allow the LLM to manage contracts: create tasks, add files,
-//! manage repositories, and handle phase transitions.
-
-use serde_json::json;
-use uuid::Uuid;
-
-use super::tools::Tool;
-
-/// Available tools for contract management
-pub static CONTRACT_TOOLS: once_cell::sync::Lazy<Vec<Tool>> = once_cell::sync::Lazy::new(|| {
- vec![
- // =============================================================================
- // Query Tools
- // =============================================================================
- Tool {
- name: "get_contract_status".to_string(),
- description: "Get an overview of the contract including current phase, file count, task count, and repository count.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {}
- }),
- },
- Tool {
- name: "list_contract_files".to_string(),
- description: "List all files in the contract with their names, descriptions, and phases.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {}
- }),
- },
- Tool {
- name: "list_contract_tasks".to_string(),
- description: "List all tasks in the contract with their names, status, and progress.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {}
- }),
- },
- Tool {
- name: "list_contract_repositories".to_string(),
- description: "List all repositories attached to the contract with their types and URLs/paths.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {}
- }),
- },
- Tool {
- name: "read_file".to_string(),
- description: "Read the full contents of a file including its body, transcript, and summary.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "file_id": {
- "type": "string",
- "description": "ID of the file to read"
- }
- },
- "required": ["file_id"]
- }),
- },
- // =============================================================================
- // File Management Tools
- // =============================================================================
- Tool {
- name: "create_empty_file".to_string(),
- description: "Create a new empty file in the contract.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "description": "Name for the new file"
- },
- "description": {
- "type": "string",
- "description": "Optional description for the file"
- }
- },
- "required": ["name"]
- }),
- },
- // =============================================================================
- // Deliverable Management Tools
- // =============================================================================
- Tool {
- name: "mark_deliverable_complete".to_string(),
- description: "Mark a phase deliverable as complete. Use this when you have verified that a deliverable requirement has been satisfied. Use get_phase_info or check_deliverables_met first to see available deliverable IDs.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "deliverable_id": {
- "type": "string",
- "description": "The ID of the deliverable to mark as complete (e.g., 'plan-document', 'pull-request', 'research-notes')"
- },
- "phase": {
- "type": "string",
- "enum": ["research", "specify", "plan", "execute", "review"],
- "description": "Phase the deliverable belongs to. Defaults to the current contract phase if not specified."
- }
- },
- "required": ["deliverable_id"]
- }),
- },
- // =============================================================================
- // Task Management Tools
- // =============================================================================
- Tool {
- name: "create_contract_task".to_string(),
- description: "Create a new task within this contract. The task will be associated with the contract and can optionally use a contract repository.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "description": "Name of the task"
- },
- "plan": {
- "type": "string",
- "description": "Detailed instructions/plan for what the task should accomplish"
- },
- "repository_url": {
- "type": "string",
- "description": "Git repository URL or local path. If not specified, uses the contract's primary repository."
- },
- "base_branch": {
- "type": "string",
- "description": "Optional base branch to start from (default: main)"
- }
- },
- "required": ["name", "plan"]
- }),
- },
- Tool {
- name: "delegate_content_generation".to_string(),
- description: "Create a task to generate substantial content instead of writing it directly. Use this for filling templates, writing documentation, generating user stories, or any substantial writing task. The task will be created and can be started separately.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "file_id": {
- "type": "string",
- "description": "ID of the file to update with generated content (optional - if not specified, creates a new task without file context)"
- },
- "instruction": {
- "type": "string",
- "description": "Clear instructions for what content should be generated"
- },
- "context": {
- "type": "string",
- "description": "Additional context to help generate appropriate content"
- }
- },
- "required": ["instruction"]
- }),
- },
- Tool {
- name: "start_task".to_string(),
- description: "Start a task that is in 'pending' status. The task will be sent to a connected daemon for execution. A daemon must be connected for this to work.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "task_id": {
- "type": "string",
- "description": "ID of the task to start"
- }
- },
- "required": ["task_id"]
- }),
- },
- // =============================================================================
- // Phase Management Tools
- // =============================================================================
- Tool {
- name: "get_phase_info".to_string(),
- description: "Get detailed information about the current phase and what it means for the contract workflow.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {}
- }),
- },
- Tool {
- name: "suggest_phase_transition".to_string(),
- description: "Analyze whether the contract is ready to advance to the NEXT phase. Returns: currentPhase, nextPhase (the phase to advance TO), readiness status, and what's missing. Use this BEFORE calling advance_phase to know exactly which phase to advance to.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {}
- }),
- },
- Tool {
- name: "advance_phase".to_string(),
- description: "Advance the contract to the NEXT phase in sequence. Phases progress: research -> specify -> plan -> execute -> review. You can ONLY advance forward one step. Always use suggest_phase_transition first to check readiness and find the correct next phase. If the contract has phase_guard enabled, this will first return a pending_confirmation status with phase deliverables for user review. Call again with confirmed=true to complete the transition, or with feedback to request changes.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "new_phase": {
- "type": "string",
- "enum": ["specify", "plan", "execute", "review"],
- "description": "The next phase to transition to. Must be exactly one step ahead of current phase (e.g., research->specify, specify->plan, plan->execute, execute->review)"
- },
- "confirmed": {
- "type": "boolean",
- "description": "Set to true to confirm the phase transition when phase_guard is enabled. If omitted or false, returns deliverables for review."
- },
- "feedback": {
- "type": "string",
- "description": "User feedback when requesting changes instead of confirming the transition. The feedback will be passed back to the task to address."
- }
- },
- "required": ["new_phase"]
- }),
- },
- // =============================================================================
- // Repository Management Tools
- // =============================================================================
- Tool {
- name: "list_daemon_directories".to_string(),
- description: "List suggested directories from connected daemons. Use this to find valid local paths when the user wants to add a local repository or configure a target path. Returns working directories and home directories from connected agents.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {}
- }),
- },
- Tool {
- name: "add_repository".to_string(),
- description: "Add a repository to the contract. Can be a remote URL, local path, or create a managed repository.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "type": {
- "type": "string",
- "enum": ["remote", "local", "managed"],
- "description": "Type of repository to add"
- },
- "name": {
- "type": "string",
- "description": "Display name for the repository"
- },
- "url": {
- "type": "string",
- "description": "Repository URL (for remote type) or local path (for local type). Not needed for managed."
- },
- "is_primary": {
- "type": "boolean",
- "description": "Whether this should be the primary repository for the contract"
- }
- },
- "required": ["type", "name"]
- }),
- },
- Tool {
- name: "set_primary_repository".to_string(),
- description: "Set a repository as the primary repository for this contract. The primary repo is used by default for new tasks.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "repository_id": {
- "type": "string",
- "description": "ID of the repository to set as primary"
- }
- },
- "required": ["repository_id"]
- }),
- },
- // =============================================================================
- // Phase Guidance Tools
- // =============================================================================
- Tool {
- name: "get_phase_checklist".to_string(),
- description: "Get a detailed checklist of phase deliverables showing what's been created vs what's expected. Includes completion percentage and suggestions for next steps.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {}
- }),
- },
- Tool {
- name: "check_deliverables_met".to_string(),
- description: "Check if all required deliverables are met for the current phase and whether the contract is ready to advance to the next phase. Returns detailed status including: deliverables_met (bool), ready_to_advance (bool), required_deliverables (list with status), missing items, and auto_progress_recommended (bool). Use this before calling advance_phase to ensure all requirements are satisfied. For simple contracts: Plan phase needs Plan document + Repository, Execute phase needs completed tasks + PR. For specification contracts: Each phase has specific required documents.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {}
- }),
- },
- // =============================================================================
- // Task Derivation Tools
- // =============================================================================
- Tool {
- name: "derive_tasks_from_file".to_string(),
- description: "Parse a file (typically Task Breakdown) to extract a list of tasks. Returns structured task data that can be used with create_chained_tasks.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "file_id": {
- "type": "string",
- "description": "ID of the file to parse tasks from (usually a Task Breakdown document)"
- }
- },
- "required": ["file_id"]
- }),
- },
- Tool {
- name: "create_chained_tasks".to_string(),
- description: "Create multiple tasks in sequence with automatic chaining. Each task will continue from the previous task's work using continue_from_task_id.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "tasks": {
- "type": "array",
- "description": "List of tasks to create in order",
- "items": {
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "description": "Task name"
- },
- "plan": {
- "type": "string",
- "description": "Task plan/instructions"
- }
- },
- "required": ["name", "plan"]
- }
- }
- },
- "required": ["tasks"]
- }),
- },
- // =============================================================================
- // Task Completion Processing Tools
- // =============================================================================
- Tool {
- name: "process_task_completion".to_string(),
- description: "Analyze a completed task's output and suggest next actions. Returns summary, affected files, and suggestions for follow-up tasks or file updates.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "task_id": {
- "type": "string",
- "description": "ID of the completed task to analyze"
- }
- },
- "required": ["task_id"]
- }),
- },
- Tool {
- name: "update_file_from_task".to_string(),
- description: "Update a contract file with information from a completed task. Useful for updating Dev Notes or Implementation Log with task summaries.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "file_id": {
- "type": "string",
- "description": "ID of the file to update"
- },
- "task_id": {
- "type": "string",
- "description": "ID of the task whose output should be added"
- },
- "section_title": {
- "type": "string",
- "description": "Optional title for the section being added (e.g., 'Task: Implement Authentication')"
- }
- },
- "required": ["file_id", "task_id"]
- }),
- },
- // =============================================================================
- // Interactive Tools
- // =============================================================================
- Tool {
- name: "ask_user".to_string(),
- description: "Ask the user one or more questions. Use this when you need clarification, want to offer choices, or need user input before proceeding. Questions can be single-select (user picks one option) or multi-select (user can pick multiple options). The question text supports markdown formatting.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "questions": {
- "type": "array",
- "description": "List of questions to ask the user",
- "items": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string",
- "description": "Unique identifier for this question"
- },
- "question": {
- "type": "string",
- "description": "The question to ask the user. Supports markdown formatting (bold, code, lists, etc.)"
- },
- "options": {
- "type": "array",
- "items": { "type": "string" },
- "description": "Multiple choice options for the user to select from"
- },
- "allowMultiple": {
- "type": "boolean",
- "description": "If true, user can select multiple options (multi-select). If false or omitted, user selects exactly one option (single-select). Default: false"
- },
- "allowCustom": {
- "type": "boolean",
- "description": "If true, user can provide a custom text answer instead of selecting from options. Default: true"
- }
- },
- "required": ["id", "question", "options"]
- }
- }
- },
- "required": ["questions"]
- }),
- },
- // =============================================================================
- // Transcript Analysis Tools
- // =============================================================================
- Tool {
- name: "analyze_transcript".to_string(),
- description: "Analyze a file's transcript to extract requirements, decisions, and action items. Returns structured analysis including speaker statistics.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "file_id": {
- "type": "string",
- "description": "ID of the file containing the transcript to analyze"
- }
- },
- "required": ["file_id"]
- }),
- },
- Tool {
- name: "create_contract_from_transcript".to_string(),
- description: "Create a new contract from an analyzed transcript. Will extract requirements, decisions, and action items and create appropriate files and tasks in the new contract.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "file_id": {
- "type": "string",
- "description": "ID of the file containing the transcript"
- },
- "name": {
- "type": "string",
- "description": "Optional name for the contract (otherwise auto-generated from analysis)"
- },
- "description": {
- "type": "string",
- "description": "Optional description for the contract (otherwise auto-generated)"
- },
- "include_requirements": {
- "type": "boolean",
- "description": "Whether to create a requirements file (default: true)"
- },
- "include_decisions": {
- "type": "boolean",
- "description": "Whether to create a decisions file (default: true)"
- },
- "include_action_items": {
- "type": "boolean",
- "description": "Whether to create tasks from action items (default: true)"
- }
- },
- "required": ["file_id"]
- }),
- },
- ]
-});
-
-/// Request for contract tool operations that require async database access
-#[derive(Debug, Clone)]
-pub enum ContractToolRequest {
- // Query operations
- GetContractStatus,
- ListContractFiles,
- ListContractTasks,
- ListContractRepositories,
- ReadFile { file_id: Uuid },
-
- // File management
- CreateEmptyFile {
- name: String,
- description: Option<String>,
- },
-
- // Deliverable management
- MarkDeliverableComplete {
- deliverable_id: String,
- phase: Option<String>,
- },
-
- // Task management
- CreateContractTask {
- name: String,
- plan: String,
- repository_url: Option<String>,
- base_branch: Option<String>,
- },
- DelegateContentGeneration {
- file_id: Option<Uuid>,
- instruction: String,
- context: Option<String>,
- },
- StartTask { task_id: Uuid },
-
- // Phase management
- GetPhaseInfo,
- SuggestPhaseTransition,
- AdvancePhase {
- new_phase: String,
- /// Whether the user has confirmed the phase transition (for phase_guard)
- confirmed: bool,
- /// User feedback when they request changes instead of confirming
- feedback: Option<String>,
- },
-
- // Repository management
- ListDaemonDirectories,
- AddRepository {
- repo_type: String,
- name: String,
- url: Option<String>,
- is_primary: bool,
- },
- SetPrimaryRepository { repository_id: Uuid },
-
- // Phase guidance
- GetPhaseChecklist,
- CheckDeliverablesMet,
-
- // Task derivation
- DeriveTasksFromFile { file_id: Uuid },
- CreateChainedTasks { tasks: Vec<ChainedTaskDef> },
-
- // Task completion processing
- ProcessTaskCompletion { task_id: Uuid },
- UpdateFileFromTask {
- file_id: Uuid,
- task_id: Uuid,
- section_title: Option<String>,
- },
-
- // Transcript analysis
- AnalyzeTranscript { file_id: Uuid },
- CreateContractFromTranscript {
- file_id: Uuid,
- name: Option<String>,
- description: Option<String>,
- include_requirements: bool,
- include_decisions: bool,
- include_action_items: bool,
- },
-
-}
-
-/// Task definition for chained task creation
-#[derive(Debug, Clone, serde::Deserialize)]
-pub struct ChainedTaskDef {
- pub name: String,
- pub plan: String,
-}
-
-/// Result from executing a contract tool
-#[derive(Debug)]
-pub struct ContractToolExecutionResult {
- pub success: bool,
- pub message: String,
- pub data: Option<serde_json::Value>,
- /// Request for async operations (handled by contract_chat handler)
- pub request: Option<ContractToolRequest>,
- /// Questions to ask the user (pauses conversation)
- pub pending_questions: Option<Vec<super::tools::UserQuestion>>,
-}
-
-/// Parse and validate a contract tool call, returning a ContractToolRequest for async handling
-pub fn parse_contract_tool_call(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- match call.name.as_str() {
- // Query operations
- "get_contract_status" => parse_get_contract_status(),
- "list_contract_files" => parse_list_contract_files(),
- "list_contract_tasks" => parse_list_contract_tasks(),
- "list_contract_repositories" => parse_list_contract_repositories(),
- "read_file" => parse_read_file(call),
-
- // File management
- "create_empty_file" => parse_create_empty_file(call),
-
- // Deliverable management
- "mark_deliverable_complete" => parse_mark_deliverable_complete(call),
-
- // Task management
- "create_contract_task" => parse_create_contract_task(call),
- "delegate_content_generation" => parse_delegate_content_generation(call),
- "start_task" => parse_start_task(call),
-
- // Phase management
- "get_phase_info" => parse_get_phase_info(),
- "suggest_phase_transition" => parse_suggest_phase_transition(),
- "advance_phase" => parse_advance_phase(call),
-
- // Repository management
- "list_daemon_directories" => parse_list_daemon_directories(),
- "add_repository" => parse_add_repository(call),
- "set_primary_repository" => parse_set_primary_repository(call),
-
- // Phase guidance
- "get_phase_checklist" => parse_get_phase_checklist(),
- "check_deliverables_met" => parse_check_deliverables_met(),
-
- // Task derivation
- "derive_tasks_from_file" => parse_derive_tasks_from_file(call),
- "create_chained_tasks" => parse_create_chained_tasks(call),
-
- // Task completion processing
- "process_task_completion" => parse_process_task_completion(call),
- "update_file_from_task" => parse_update_file_from_task(call),
-
- // Interactive tools
- "ask_user" => parse_ask_user(call),
-
- // Transcript analysis tools
- "analyze_transcript" => parse_analyze_transcript(call),
- "create_contract_from_transcript" => parse_create_contract_from_transcript(call),
-
- _ => ContractToolExecutionResult {
- success: false,
- message: format!("Unknown contract tool: {}", call.name),
- data: None,
- request: None,
- pending_questions: None,
- },
- }
-}
-
-// =============================================================================
-// Query Tool Parsing
-// =============================================================================
-
-fn parse_get_contract_status() -> ContractToolExecutionResult {
- ContractToolExecutionResult {
- success: true,
- message: "Getting contract status...".to_string(),
- data: None,
- request: Some(ContractToolRequest::GetContractStatus),
- pending_questions: None,
- }
-}
-
-fn parse_list_contract_files() -> ContractToolExecutionResult {
- ContractToolExecutionResult {
- success: true,
- message: "Listing contract files...".to_string(),
- data: None,
- request: Some(ContractToolRequest::ListContractFiles),
- pending_questions: None,
- }
-}
-
-fn parse_list_contract_tasks() -> ContractToolExecutionResult {
- ContractToolExecutionResult {
- success: true,
- message: "Listing contract tasks...".to_string(),
- data: None,
- request: Some(ContractToolRequest::ListContractTasks),
- pending_questions: None,
- }
-}
-
-fn parse_list_contract_repositories() -> ContractToolExecutionResult {
- ContractToolExecutionResult {
- success: true,
- message: "Listing contract repositories...".to_string(),
- data: None,
- request: Some(ContractToolRequest::ListContractRepositories),
- pending_questions: None,
- }
-}
-
-fn parse_read_file(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let file_id = parse_uuid_arg(call, "file_id");
- let Some(file_id) = file_id else {
- return error_result("Missing or invalid required parameter: file_id");
- };
-
- ContractToolExecutionResult {
- success: true,
- message: "Reading file...".to_string(),
- data: None,
- request: Some(ContractToolRequest::ReadFile { file_id }),
- pending_questions: None,
- }
-}
-
-// =============================================================================
-// File Management Tool Parsing
-// =============================================================================
-
-fn parse_create_empty_file(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let name = call.arguments.get("name").and_then(|v| v.as_str());
-
- let Some(name) = name else {
- return error_result("Missing required parameter: name");
- };
-
- let description = call
- .arguments
- .get("description")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string());
-
- ContractToolExecutionResult {
- success: true,
- message: format!("Creating empty file '{}'...", name),
- data: None,
- request: Some(ContractToolRequest::CreateEmptyFile {
- name: name.to_string(),
- description,
- }),
- pending_questions: None,
- }
-}
-
-// =============================================================================
-// Deliverable Management Tool Parsing
-// =============================================================================
-
-fn parse_mark_deliverable_complete(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let deliverable_id = call
- .arguments
- .get("deliverable_id")
- .and_then(|v| v.as_str());
-
- let Some(deliverable_id) = deliverable_id else {
- return error_result("Missing required parameter: deliverable_id");
- };
-
- let phase = call
- .arguments
- .get("phase")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string());
-
- ContractToolExecutionResult {
- success: true,
- message: format!("Marking deliverable '{}' as complete...", deliverable_id),
- data: None,
- request: Some(ContractToolRequest::MarkDeliverableComplete {
- deliverable_id: deliverable_id.to_string(),
- phase,
- }),
- pending_questions: None,
- }
-}
-
-// =============================================================================
-// Task Management Tool Parsing
-// =============================================================================
-
-fn parse_create_contract_task(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let name = call.arguments.get("name").and_then(|v| v.as_str());
- let plan = call.arguments.get("plan").and_then(|v| v.as_str());
-
- let Some(name) = name else {
- return error_result("Missing required parameter: name");
- };
- let Some(plan) = plan else {
- return error_result("Missing required parameter: plan");
- };
-
- let repository_url = call
- .arguments
- .get("repository_url")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string());
-
- let base_branch = call
- .arguments
- .get("base_branch")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string());
-
- ContractToolExecutionResult {
- success: true,
- message: format!("Creating task '{}'...", name),
- data: None,
- request: Some(ContractToolRequest::CreateContractTask {
- name: name.to_string(),
- plan: plan.to_string(),
- repository_url,
- base_branch,
- }),
- pending_questions: None,
- }
-}
-
-fn parse_delegate_content_generation(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let instruction = call.arguments.get("instruction").and_then(|v| v.as_str());
-
- let Some(instruction) = instruction else {
- return error_result("Missing required parameter: instruction");
- };
-
- let file_id = parse_uuid_arg(call, "file_id");
- let context = call
- .arguments
- .get("context")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string());
-
- ContractToolExecutionResult {
- success: true,
- message: "Creating content generation task...".to_string(),
- data: None,
- request: Some(ContractToolRequest::DelegateContentGeneration {
- file_id,
- instruction: instruction.to_string(),
- context,
- }),
- pending_questions: None,
- }
-}
-
-fn parse_start_task(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let task_id = parse_uuid_arg(call, "task_id");
-
- let Some(task_id) = task_id else {
- return error_result("Missing or invalid required parameter: task_id");
- };
-
- ContractToolExecutionResult {
- success: true,
- message: "Starting task...".to_string(),
- data: None,
- request: Some(ContractToolRequest::StartTask { task_id }),
- pending_questions: None,
- }
-}
-
-// =============================================================================
-// Phase Management Tool Parsing
-// =============================================================================
-
-fn parse_get_phase_info() -> ContractToolExecutionResult {
- ContractToolExecutionResult {
- success: true,
- message: "Getting phase information...".to_string(),
- data: None,
- request: Some(ContractToolRequest::GetPhaseInfo),
- pending_questions: None,
- }
-}
-
-fn parse_suggest_phase_transition() -> ContractToolExecutionResult {
- ContractToolExecutionResult {
- success: true,
- message: "Analyzing phase transition readiness...".to_string(),
- data: None,
- request: Some(ContractToolRequest::SuggestPhaseTransition),
- pending_questions: None,
- }
-}
-
-fn parse_advance_phase(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let new_phase = call.arguments.get("new_phase").and_then(|v| v.as_str());
-
- let Some(new_phase) = new_phase else {
- return error_result("Missing required parameter: new_phase");
- };
-
- let valid_phases = ["research", "specify", "plan", "execute", "review"];
- if !valid_phases.contains(&new_phase) {
- return error_result("Invalid phase. Must be one of: research, specify, plan, execute, review");
- }
-
- // Parse optional confirmed flag (defaults to false for initial phase_guard check)
- let confirmed = call
- .arguments
- .get("confirmed")
- .and_then(|v| v.as_bool())
- .unwrap_or(false);
-
- // Parse optional feedback (for when user requests changes)
- let feedback = call
- .arguments
- .get("feedback")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string());
-
- ContractToolExecutionResult {
- success: true,
- message: format!("Advancing to '{}' phase...", new_phase),
- data: None,
- request: Some(ContractToolRequest::AdvancePhase {
- new_phase: new_phase.to_string(),
- confirmed,
- feedback,
- }),
- pending_questions: None,
- }
-}
-
-// =============================================================================
-// Repository Management Tool Parsing
-// =============================================================================
-
-fn parse_list_daemon_directories() -> ContractToolExecutionResult {
- ContractToolExecutionResult {
- success: true,
- message: "Listing daemon directories...".to_string(),
- data: None,
- request: Some(ContractToolRequest::ListDaemonDirectories),
- pending_questions: None,
- }
-}
-
-fn parse_add_repository(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let repo_type = call.arguments.get("type").and_then(|v| v.as_str());
- let name = call.arguments.get("name").and_then(|v| v.as_str());
-
- let Some(repo_type) = repo_type else {
- return error_result("Missing required parameter: type");
- };
- let Some(name) = name else {
- return error_result("Missing required parameter: name");
- };
-
- let valid_types = ["remote", "local", "managed"];
- if !valid_types.contains(&repo_type) {
- return error_result("Invalid type. Must be one of: remote, local, managed");
- }
-
- let url = call
- .arguments
- .get("url")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string());
-
- // Validate URL is provided for remote and local types
- if (repo_type == "remote" || repo_type == "local") && url.is_none() {
- return error_result("URL/path is required for remote and local repository types");
- }
-
- let is_primary = call
- .arguments
- .get("is_primary")
- .and_then(|v| v.as_bool())
- .unwrap_or(false);
-
- ContractToolExecutionResult {
- success: true,
- message: format!("Adding {} repository '{}'...", repo_type, name),
- data: None,
- request: Some(ContractToolRequest::AddRepository {
- repo_type: repo_type.to_string(),
- name: name.to_string(),
- url,
- is_primary,
- }),
- pending_questions: None,
- }
-}
-
-fn parse_set_primary_repository(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let repository_id = parse_uuid_arg(call, "repository_id");
- let Some(repository_id) = repository_id else {
- return error_result("Missing or invalid required parameter: repository_id");
- };
-
- ContractToolExecutionResult {
- success: true,
- message: "Setting primary repository...".to_string(),
- data: None,
- request: Some(ContractToolRequest::SetPrimaryRepository { repository_id }),
- pending_questions: None,
- }
-}
-
-// =============================================================================
-// Interactive Tool Parsing
-// =============================================================================
-
-fn parse_ask_user(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let questions_value = call.arguments.get("questions");
-
- let Some(questions_array) = questions_value.and_then(|v| v.as_array()) else {
- return error_result("Missing or invalid 'questions' parameter");
- };
-
- let mut questions: Vec<super::tools::UserQuestion> = Vec::new();
-
- for q in questions_array {
- let id = q.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();
- let question = q.get("question").and_then(|v| v.as_str()).unwrap_or("").to_string();
- let options: Vec<String> = q
- .get("options")
- .and_then(|v| v.as_array())
- .map(|arr| {
- arr.iter()
- .filter_map(|o| o.as_str())
- .map(|s| s.to_string())
- .collect()
- })
- .unwrap_or_default();
- let allow_multiple = q.get("allowMultiple").and_then(|v| v.as_bool()).unwrap_or(false);
- let allow_custom = q.get("allowCustom").and_then(|v| v.as_bool()).unwrap_or(true);
-
- if id.is_empty() || question.is_empty() || options.is_empty() {
- continue;
- }
-
- questions.push(super::tools::UserQuestion {
- id,
- question,
- options,
- allow_multiple,
- allow_custom,
- });
- }
-
- if questions.is_empty() {
- return error_result("No valid questions provided");
- }
-
- let question_count = questions.len();
- ContractToolExecutionResult {
- success: true,
- message: format!("Asking user {} question(s). Waiting for response...", question_count),
- data: None,
- request: None,
- pending_questions: Some(questions),
- }
-}
-
-// =============================================================================
-// Phase Guidance Tool Parsing
-// =============================================================================
-
-fn parse_get_phase_checklist() -> ContractToolExecutionResult {
- ContractToolExecutionResult {
- success: true,
- message: "Getting phase checklist...".to_string(),
- data: None,
- request: Some(ContractToolRequest::GetPhaseChecklist),
- pending_questions: None,
- }
-}
-
-fn parse_check_deliverables_met() -> ContractToolExecutionResult {
- ContractToolExecutionResult {
- success: true,
- message: "Checking if deliverables are met...".to_string(),
- data: None,
- request: Some(ContractToolRequest::CheckDeliverablesMet),
- pending_questions: None,
- }
-}
-
-// =============================================================================
-// Task Derivation Tool Parsing
-// =============================================================================
-
-fn parse_derive_tasks_from_file(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let file_id = parse_uuid_arg(call, "file_id");
- let Some(file_id) = file_id else {
- return error_result("Missing or invalid required parameter: file_id");
- };
-
- ContractToolExecutionResult {
- success: true,
- message: "Deriving tasks from file...".to_string(),
- data: None,
- request: Some(ContractToolRequest::DeriveTasksFromFile { file_id }),
- pending_questions: None,
- }
-}
-
-fn parse_create_chained_tasks(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let tasks_value = call.arguments.get("tasks");
-
- let Some(tasks_array) = tasks_value.and_then(|v| v.as_array()) else {
- return error_result("Missing or invalid 'tasks' parameter");
- };
-
- let mut tasks: Vec<ChainedTaskDef> = Vec::new();
-
- for task in tasks_array {
- let name = task.get("name").and_then(|v| v.as_str());
- let plan = task.get("plan").and_then(|v| v.as_str());
-
- match (name, plan) {
- (Some(n), Some(p)) => {
- tasks.push(ChainedTaskDef {
- name: n.to_string(),
- plan: p.to_string(),
- });
- }
- _ => {
- return error_result("Each task must have 'name' and 'plan' fields");
- }
- }
- }
-
- if tasks.is_empty() {
- return error_result("No valid tasks provided");
- }
-
- let task_count = tasks.len();
- ContractToolExecutionResult {
- success: true,
- message: format!("Creating {} chained task(s)...", task_count),
- data: None,
- request: Some(ContractToolRequest::CreateChainedTasks { tasks }),
- pending_questions: None,
- }
-}
-
-// =============================================================================
-// Task Completion Processing Tool Parsing
-// =============================================================================
-
-fn parse_process_task_completion(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let task_id = parse_uuid_arg(call, "task_id");
- let Some(task_id) = task_id else {
- return error_result("Missing or invalid required parameter: task_id");
- };
-
- ContractToolExecutionResult {
- success: true,
- message: "Processing task completion...".to_string(),
- data: None,
- request: Some(ContractToolRequest::ProcessTaskCompletion { task_id }),
- pending_questions: None,
- }
-}
-
-fn parse_update_file_from_task(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let file_id = parse_uuid_arg(call, "file_id");
- let task_id = parse_uuid_arg(call, "task_id");
-
- let Some(file_id) = file_id else {
- return error_result("Missing or invalid required parameter: file_id");
- };
- let Some(task_id) = task_id else {
- return error_result("Missing or invalid required parameter: task_id");
- };
-
- let section_title = call
- .arguments
- .get("section_title")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string());
-
- ContractToolExecutionResult {
- success: true,
- message: "Updating file from task...".to_string(),
- data: None,
- request: Some(ContractToolRequest::UpdateFileFromTask {
- file_id,
- task_id,
- section_title,
- }),
- pending_questions: None,
- }
-}
-
-// =============================================================================
-// Transcript Analysis Tool Parsing
-// =============================================================================
-
-fn parse_analyze_transcript(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let file_id = parse_uuid_arg(call, "file_id");
- let Some(file_id) = file_id else {
- return error_result("Missing or invalid required parameter: file_id");
- };
-
- ContractToolExecutionResult {
- success: true,
- message: "Analyzing transcript...".to_string(),
- data: None,
- request: Some(ContractToolRequest::AnalyzeTranscript { file_id }),
- pending_questions: None,
- }
-}
-
-fn parse_create_contract_from_transcript(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let file_id = parse_uuid_arg(call, "file_id");
- let Some(file_id) = file_id else {
- return error_result("Missing or invalid required parameter: file_id");
- };
-
- let name = call.arguments.get("name").and_then(|v| v.as_str()).map(|s| s.to_string());
- let description = call.arguments.get("description").and_then(|v| v.as_str()).map(|s| s.to_string());
- let include_requirements = call.arguments.get("include_requirements").and_then(|v| v.as_bool()).unwrap_or(true);
- let include_decisions = call.arguments.get("include_decisions").and_then(|v| v.as_bool()).unwrap_or(true);
- let include_action_items = call.arguments.get("include_action_items").and_then(|v| v.as_bool()).unwrap_or(true);
-
- ContractToolExecutionResult {
- success: true,
- message: "Creating contract from transcript...".to_string(),
- data: None,
- request: Some(ContractToolRequest::CreateContractFromTranscript {
- file_id,
- name,
- description,
- include_requirements,
- include_decisions,
- include_action_items,
- }),
- pending_questions: None,
- }
-}
-
-// =============================================================================
-// Helper Functions
-// =============================================================================
-
-fn parse_uuid_arg(call: &super::tools::ToolCall, key: &str) -> Option<Uuid> {
- call.arguments
- .get(key)
- .and_then(|v| v.as_str())
- .and_then(|s| Uuid::parse_str(s).ok())
-}
-
-fn error_result(message: &str) -> ContractToolExecutionResult {
- ContractToolExecutionResult {
- success: false,
- message: message.to_string(),
- data: None,
- request: None,
- pending_questions: None,
- }
-}
diff --git a/makima/src/llm/discuss_tools.rs b/makima/src/llm/discuss_tools.rs
deleted file mode 100644
index 7330db3..0000000
--- a/makima/src/llm/discuss_tools.rs
+++ /dev/null
@@ -1,210 +0,0 @@
-//! Tool definitions for contract discussion via LLM.
-//!
-//! These tools allow Makima to help users define and create contracts
-//! through natural conversation.
-
-use serde_json::json;
-
-use super::tools::Tool;
-
-/// Available tools for contract discussion
-pub static DISCUSS_TOOLS: once_cell::sync::Lazy<Vec<Tool>> = once_cell::sync::Lazy::new(|| {
- vec![
- Tool {
- name: "create_contract".to_string(),
- description: "Create a new contract based on the discussion. Only call this when the user has confirmed they're ready to create the contract.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "description": "Name for the contract"
- },
- "description": {
- "type": "string",
- "description": "Detailed description of what the contract is for"
- },
- "contract_type": {
- "type": "string",
- "enum": ["simple", "specification", "execute"],
- "description": "Type of contract workflow"
- },
- "repository_url": {
- "type": "string",
- "description": "Optional repository URL if discussed"
- },
- "local_only": {
- "type": "boolean",
- "description": "If true, tasks won't auto-push or create PRs"
- }
- },
- "required": ["name", "description", "contract_type"]
- }),
- },
- Tool {
- name: "ask_clarification".to_string(),
- description: "Ask the user a clarifying question with multiple choice options.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "question": {
- "type": "string",
- "description": "The question to ask"
- },
- "options": {
- "type": "array",
- "items": { "type": "string" },
- "description": "Multiple choice options"
- },
- "allow_custom": {
- "type": "boolean",
- "description": "Allow user to provide a custom answer"
- }
- },
- "required": ["question", "options"]
- }),
- },
- ]
-});
-
-/// Request for discussion tool operations that require async database access
-#[derive(Debug, Clone)]
-pub enum DiscussToolRequest {
- /// Create a new contract
- CreateContract {
- name: String,
- description: String,
- contract_type: String,
- repository_url: Option<String>,
- local_only: bool,
- },
-}
-
-/// Result from executing a discussion tool
-#[derive(Debug)]
-pub struct DiscussToolExecutionResult {
- pub success: bool,
- pub message: String,
- pub data: Option<serde_json::Value>,
- /// Request for async operations (handled by discuss handler)
- pub request: Option<DiscussToolRequest>,
- /// Questions to ask the user (pauses conversation)
- pub pending_questions: Option<Vec<super::tools::UserQuestion>>,
-}
-
-/// Parse and validate a discussion tool call, returning a DiscussToolRequest for async handling
-pub fn parse_discuss_tool_call(call: &super::tools::ToolCall) -> DiscussToolExecutionResult {
- match call.name.as_str() {
- "create_contract" => parse_create_contract(call),
- "ask_clarification" => parse_ask_clarification(call),
- _ => DiscussToolExecutionResult {
- success: false,
- message: format!("Unknown discussion tool: {}", call.name),
- data: None,
- request: None,
- pending_questions: None,
- },
- }
-}
-
-fn parse_create_contract(call: &super::tools::ToolCall) -> DiscussToolExecutionResult {
- let name = call.arguments.get("name").and_then(|v| v.as_str());
- let description = call.arguments.get("description").and_then(|v| v.as_str());
- let contract_type = call.arguments.get("contract_type").and_then(|v| v.as_str());
-
- let Some(name) = name else {
- return error_result("Missing required parameter: name");
- };
- let Some(description) = description else {
- return error_result("Missing required parameter: description");
- };
- let Some(contract_type) = contract_type else {
- return error_result("Missing required parameter: contract_type");
- };
-
- let valid_types = ["simple", "specification", "execute"];
- if !valid_types.contains(&contract_type) {
- return error_result("Invalid contract_type. Must be one of: simple, specification, execute");
- }
-
- let repository_url = call
- .arguments
- .get("repository_url")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string());
-
- let local_only = call
- .arguments
- .get("local_only")
- .and_then(|v| v.as_bool())
- .unwrap_or(false);
-
- DiscussToolExecutionResult {
- success: true,
- message: format!("Creating contract '{}'...", name),
- data: None,
- request: Some(DiscussToolRequest::CreateContract {
- name: name.to_string(),
- description: description.to_string(),
- contract_type: contract_type.to_string(),
- repository_url,
- local_only,
- }),
- pending_questions: None,
- }
-}
-
-fn parse_ask_clarification(call: &super::tools::ToolCall) -> DiscussToolExecutionResult {
- let question = call.arguments.get("question").and_then(|v| v.as_str());
- let options = call.arguments.get("options").and_then(|v| v.as_array());
-
- let Some(question) = question else {
- return error_result("Missing required parameter: question");
- };
- let Some(options) = options else {
- return error_result("Missing required parameter: options");
- };
-
- let options: Vec<String> = options
- .iter()
- .filter_map(|o| o.as_str())
- .map(|s| s.to_string())
- .collect();
-
- if options.is_empty() {
- return error_result("Options array cannot be empty");
- }
-
- let allow_custom = call
- .arguments
- .get("allow_custom")
- .and_then(|v| v.as_bool())
- .unwrap_or(true);
-
- // Create a UserQuestion for the ask_clarification tool
- let user_question = super::tools::UserQuestion {
- id: "clarification".to_string(),
- question: question.to_string(),
- options,
- allow_multiple: false,
- allow_custom,
- };
-
- DiscussToolExecutionResult {
- success: true,
- message: format!("Asking clarification: {}", question),
- data: None,
- request: None,
- pending_questions: Some(vec![user_question]),
- }
-}
-
-fn error_result(message: &str) -> DiscussToolExecutionResult {
- DiscussToolExecutionResult {
- success: false,
- message: message.to_string(),
- data: None,
- request: None,
- pending_questions: None,
- }
-}
diff --git a/makima/src/llm/groq.rs b/makima/src/llm/groq.rs
deleted file mode 100644
index ee01fcf..0000000
--- a/makima/src/llm/groq.rs
+++ /dev/null
@@ -1,177 +0,0 @@
-//! Groq API client for LLM tool calling.
-
-use serde::{Deserialize, Serialize};
-use thiserror::Error;
-
-use super::tools::{Tool, ToolCall};
-
-const GROQ_API_URL: &str = "https://api.groq.com/openai/v1/chat/completions";
-const MODEL: &str = "moonshotai/kimi-k2-instruct-0905";
-
-#[derive(Debug, Error)]
-pub enum GroqError {
- #[error("HTTP request failed: {0}")]
- Request(#[from] reqwest::Error),
- #[error("API error: {0}")]
- Api(String),
- #[error("Missing API key")]
- MissingApiKey,
-}
-
-#[derive(Debug, Clone)]
-pub struct GroqClient {
- api_key: String,
- client: reqwest::Client,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct Message {
- pub role: String,
- pub content: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub tool_calls: Option<Vec<ToolCallResponse>>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub tool_call_id: Option<String>,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct ToolCallResponse {
- pub id: String,
- #[serde(rename = "type")]
- pub call_type: String,
- pub function: FunctionCall,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct FunctionCall {
- pub name: String,
- pub arguments: String,
-}
-
-#[derive(Debug, Serialize)]
-struct ChatRequest {
- model: String,
- messages: Vec<Message>,
- tools: Vec<ToolDefinition>,
- tool_choice: String,
-}
-
-#[derive(Debug, Serialize)]
-struct ToolDefinition {
- #[serde(rename = "type")]
- tool_type: String,
- function: FunctionDefinition,
-}
-
-#[derive(Debug, Serialize)]
-struct FunctionDefinition {
- name: String,
- description: String,
- parameters: serde_json::Value,
-}
-
-#[derive(Debug, Deserialize)]
-struct ChatResponse {
- choices: Vec<Choice>,
-}
-
-#[derive(Debug, Deserialize)]
-struct Choice {
- message: MessageResponse,
- finish_reason: String,
-}
-
-#[derive(Debug, Deserialize)]
-struct MessageResponse {
- role: String,
- content: Option<String>,
- tool_calls: Option<Vec<ToolCallResponse>>,
-}
-
-#[derive(Debug)]
-pub struct ChatResult {
- pub content: Option<String>,
- pub tool_calls: Vec<ToolCall>,
- /// Raw tool call responses for including in subsequent messages
- pub raw_tool_calls: Vec<ToolCallResponse>,
- pub finish_reason: String,
-}
-
-impl GroqClient {
- pub fn new(api_key: String) -> Self {
- Self {
- api_key,
- client: reqwest::Client::new(),
- }
- }
-
- pub fn from_env() -> Result<Self, GroqError> {
- let api_key = std::env::var("GROQ_API_KEY").map_err(|_| GroqError::MissingApiKey)?;
- Ok(Self::new(api_key))
- }
-
- pub async fn chat_with_tools(
- &self,
- messages: Vec<Message>,
- tools: &[Tool],
- ) -> Result<ChatResult, GroqError> {
- let tool_definitions: Vec<ToolDefinition> = tools
- .iter()
- .map(|t| ToolDefinition {
- tool_type: "function".to_string(),
- function: FunctionDefinition {
- name: t.name.clone(),
- description: t.description.clone(),
- parameters: t.parameters.clone(),
- },
- })
- .collect();
-
- let request = ChatRequest {
- model: MODEL.to_string(),
- messages,
- tools: tool_definitions,
- tool_choice: "auto".to_string(),
- };
-
- let response = self
- .client
- .post(GROQ_API_URL)
- .header("Authorization", format!("Bearer {}", self.api_key))
- .header("Content-Type", "application/json")
- .json(&request)
- .send()
- .await?;
-
- if !response.status().is_success() {
- let error_text = response.text().await.unwrap_or_default();
- return Err(GroqError::Api(error_text));
- }
-
- let chat_response: ChatResponse = response.json().await?;
-
- let choice = chat_response
- .choices
- .into_iter()
- .next()
- .ok_or_else(|| GroqError::Api("No choices in response".to_string()))?;
-
- let raw_tool_calls = choice.message.tool_calls.unwrap_or_default();
-
- let tool_calls = raw_tool_calls
- .iter()
- .map(|tc| ToolCall {
- id: tc.id.clone(),
- name: tc.function.name.clone(),
- arguments: serde_json::from_str(&tc.function.arguments).unwrap_or_default(),
- })
- .collect();
-
- Ok(ChatResult {
- content: choice.message.content,
- tool_calls,
- raw_tool_calls,
- finish_reason: choice.finish_reason,
- })
- }
-}
diff --git a/makima/src/llm/markdown.rs b/makima/src/llm/markdown.rs
deleted file mode 100644
index 482dc8c..0000000
--- a/makima/src/llm/markdown.rs
+++ /dev/null
@@ -1,334 +0,0 @@
-//! Markdown conversion utilities for BodyElement arrays.
-//!
-//! Provides bidirectional conversion between structured BodyElement[] and markdown strings.
-
-use crate::db::models::BodyElement;
-
-/// Convert a slice of BodyElements to a markdown string.
-///
-/// Handles:
-/// - Headings: `# heading` through `###### heading` based on level
-/// - Paragraphs: plain text with blank lines between
-/// - Code blocks: ````language\ncontent\n````
-/// - Lists: ordered (1. 2. 3.) and unordered (- - -)
-/// - Charts: rendered as fenced JSON with chart type
-/// - Images: rendered as markdown image syntax
-pub fn body_to_markdown(elements: &[BodyElement]) -> String {
- elements
- .iter()
- .filter_map(|elem| match elem {
- BodyElement::Heading { level, text } => {
- let hashes = "#".repeat((*level).min(6) as usize);
- Some(format!("{} {}", hashes, text))
- }
- BodyElement::Paragraph { text } => Some(text.clone()),
- BodyElement::Code { language, content } => {
- let lang = language.as_deref().unwrap_or("");
- Some(format!("```{}\n{}\n```", lang, content))
- }
- BodyElement::List { ordered, items } => {
- let list: Vec<String> = items
- .iter()
- .enumerate()
- .map(|(i, item)| {
- if *ordered {
- format!("{}. {}", i + 1, item)
- } else {
- format!("- {}", item)
- }
- })
- .collect();
- Some(list.join("\n"))
- }
- BodyElement::Chart {
- chart_type,
- title,
- data,
- config: _,
- } => {
- // Render chart as a fenced block with metadata
- let title_str = title
- .as_ref()
- .map(|t| format!(" - {}", t))
- .unwrap_or_default();
- let data_str = serde_json::to_string_pretty(data).unwrap_or_default();
- Some(format!(
- "```chart:{:?}{}\n{}\n```",
- chart_type, title_str, data_str
- ))
- }
- BodyElement::Image { src, alt, caption } => {
- let alt_text = alt.as_deref().unwrap_or("image");
- let caption_str = caption
- .as_ref()
- .map(|c| format!("\n*{}*", c))
- .unwrap_or_default();
- Some(format!("![{}]({}){}", alt_text, src, caption_str))
- }
- // Markdown elements output their content directly - it's already markdown
- BodyElement::Markdown { content } => Some(content.clone()),
- })
- .collect::<Vec<_>>()
- .join("\n\n")
-}
-
-/// Parse a markdown string into a vector of BodyElements.
-///
-/// Handles:
-/// - Headings: lines starting with # through ######
-/// - Code blocks: ````language ... ````
-/// - Ordered lists: lines starting with 1. 2. etc.
-/// - Unordered lists: lines starting with - or *
-/// - Paragraphs: all other non-empty lines
-pub fn markdown_to_body(markdown: &str) -> Vec<BodyElement> {
- let mut elements = Vec::new();
- let lines: Vec<&str> = markdown.lines().collect();
- let mut i = 0;
-
- while i < lines.len() {
- let line = lines[i];
- let trimmed = line.trim();
-
- // Skip empty lines
- if trimmed.is_empty() {
- i += 1;
- continue;
- }
-
- // Check for code blocks
- if trimmed.starts_with("```") {
- let language = trimmed.trim_start_matches('`').trim();
- let language = if language.is_empty() {
- None
- } else {
- Some(language.to_string())
- };
-
- let mut content_lines = Vec::new();
- i += 1;
-
- // Collect content until closing ```
- while i < lines.len() && !lines[i].trim().starts_with("```") {
- content_lines.push(lines[i]);
- i += 1;
- }
-
- // Skip the closing ```
- if i < lines.len() {
- i += 1;
- }
-
- elements.push(BodyElement::Code {
- language,
- content: content_lines.join("\n"),
- });
- continue;
- }
-
- // Check for headings
- if trimmed.starts_with('#') {
- let level = trimmed.chars().take_while(|&c| c == '#').count() as u8;
- let text = trimmed.trim_start_matches('#').trim().to_string();
- elements.push(BodyElement::Heading { level, text });
- i += 1;
- continue;
- }
-
- // Check for unordered lists (- or *)
- if trimmed.starts_with("- ") || trimmed.starts_with("* ") {
- let mut items = Vec::new();
- while i < lines.len() {
- let current = lines[i].trim();
- if current.starts_with("- ") || current.starts_with("* ") {
- items.push(current[2..].to_string());
- i += 1;
- } else if current.is_empty() {
- i += 1;
- break;
- } else {
- break;
- }
- }
- elements.push(BodyElement::List {
- ordered: false,
- items,
- });
- continue;
- }
-
- // Check for ordered lists (1. 2. etc.)
- if let Some(rest) = try_parse_ordered_list_item(trimmed) {
- let mut items = Vec::new();
- items.push(rest.to_string());
- i += 1;
-
- while i < lines.len() {
- let current = lines[i].trim();
- if let Some(item_rest) = try_parse_ordered_list_item(current) {
- items.push(item_rest.to_string());
- i += 1;
- } else if current.is_empty() {
- i += 1;
- break;
- } else {
- break;
- }
- }
- elements.push(BodyElement::List {
- ordered: true,
- items,
- });
- continue;
- }
-
- // Default: paragraph (collect consecutive non-empty lines)
- let mut para_lines = Vec::new();
- while i < lines.len() {
- let current = lines[i].trim();
- if current.is_empty()
- || current.starts_with('#')
- || current.starts_with("```")
- || current.starts_with("- ")
- || current.starts_with("* ")
- || try_parse_ordered_list_item(current).is_some()
- {
- break;
- }
- para_lines.push(current);
- i += 1;
- }
-
- if !para_lines.is_empty() {
- elements.push(BodyElement::Paragraph {
- text: para_lines.join(" "),
- });
- }
- }
-
- elements
-}
-
-/// Try to parse an ordered list item (e.g., "1. Item text")
-/// Returns the text after the number and period, or None if not a list item.
-fn try_parse_ordered_list_item(s: &str) -> Option<&str> {
- let mut chars = s.char_indices();
-
- // Must start with a digit
- let (_, first) = chars.next()?;
- if !first.is_ascii_digit() {
- return None;
- }
-
- // Consume remaining digits
- let mut last_digit_end = 1;
- for (idx, c) in chars.by_ref() {
- if c.is_ascii_digit() {
- last_digit_end = idx + 1;
- } else if c == '.' {
- // Found the period - check for space after
- let rest = &s[last_digit_end + 1..];
- let rest = rest.trim_start();
- if !rest.is_empty() || s.ends_with(". ") {
- return Some(rest);
- }
- return None;
- } else {
- return None;
- }
- }
-
- None
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_body_to_markdown_heading() {
- let elements = vec![BodyElement::Heading {
- level: 2,
- text: "Hello World".to_string(),
- }];
- assert_eq!(body_to_markdown(&elements), "## Hello World");
- }
-
- #[test]
- fn test_body_to_markdown_paragraph() {
- let elements = vec![BodyElement::Paragraph {
- text: "This is a paragraph.".to_string(),
- }];
- assert_eq!(body_to_markdown(&elements), "This is a paragraph.");
- }
-
- #[test]
- fn test_body_to_markdown_code() {
- let elements = vec![BodyElement::Code {
- language: Some("rust".to_string()),
- content: "fn main() {}".to_string(),
- }];
- assert_eq!(
- body_to_markdown(&elements),
- "```rust\nfn main() {}\n```"
- );
- }
-
- #[test]
- fn test_body_to_markdown_list() {
- let elements = vec![BodyElement::List {
- ordered: false,
- items: vec!["Item 1".to_string(), "Item 2".to_string()],
- }];
- assert_eq!(body_to_markdown(&elements), "- Item 1\n- Item 2");
- }
-
- #[test]
- fn test_markdown_to_body_heading() {
- let md = "## Hello World";
- let elements = markdown_to_body(md);
- assert_eq!(elements.len(), 1);
- match &elements[0] {
- BodyElement::Heading { level, text } => {
- assert_eq!(*level, 2);
- assert_eq!(text, "Hello World");
- }
- _ => panic!("Expected Heading"),
- }
- }
-
- #[test]
- fn test_markdown_to_body_code() {
- let md = "```rust\nfn main() {}\n```";
- let elements = markdown_to_body(md);
- assert_eq!(elements.len(), 1);
- match &elements[0] {
- BodyElement::Code { language, content } => {
- assert_eq!(language.as_deref(), Some("rust"));
- assert_eq!(content, "fn main() {}");
- }
- _ => panic!("Expected Code"),
- }
- }
-
- #[test]
- fn test_roundtrip() {
- let original = vec![
- BodyElement::Heading {
- level: 1,
- text: "Title".to_string(),
- },
- BodyElement::Paragraph {
- text: "Some text here.".to_string(),
- },
- BodyElement::List {
- ordered: false,
- items: vec!["A".to_string(), "B".to_string()],
- },
- ];
-
- let markdown = body_to_markdown(&original);
- let parsed = markdown_to_body(&markdown);
-
- assert_eq!(parsed.len(), 3);
- }
-}
diff --git a/makima/src/llm/mesh_tools.rs b/makima/src/llm/mesh_tools.rs
deleted file mode 100644
index 8bddf71..0000000
--- a/makima/src/llm/mesh_tools.rs
+++ /dev/null
@@ -1,1411 +0,0 @@
-//! Tool definitions for task mesh orchestration via LLM.
-//!
-//! These tools allow the LLM to create, manage, and coordinate tasks across
-//! connected daemons running Claude Code containers.
-
-use serde_json::json;
-use uuid::Uuid;
-
-use super::tools::Tool;
-
-/// Available tools for mesh/task orchestration
-pub static MESH_TOOLS: once_cell::sync::Lazy<Vec<Tool>> = once_cell::sync::Lazy::new(|| {
- vec![
- // =============================================================================
- // Task Lifecycle Tools
- // =============================================================================
- Tool {
- name: "create_task".to_string(),
- description: "Create a new task (or subtask if parent_task_id provided). The task will be in 'pending' status until run_task is called.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "description": "Name of the task"
- },
- "plan": {
- "type": "string",
- "description": "Detailed instructions/plan for what the task should accomplish"
- },
- "parent_task_id": {
- "type": "string",
- "description": "Optional parent task ID to create this as a subtask"
- },
- "repository_url": {
- "type": "string",
- "description": "Git repository URL or local path for the task (required)"
- },
- "base_branch": {
- "type": "string",
- "description": "Optional base branch to start from (default: main)"
- },
- "merge_mode": {
- "type": "string",
- "enum": ["pr", "auto", "manual"],
- "description": "How to handle completion: 'pr' creates PR, 'auto' auto-merges, 'manual' leaves changes for review"
- },
- "priority": {
- "type": "integer",
- "description": "Task priority (higher = more important, default: 0)"
- }
- },
- "required": ["name", "plan", "repository_url"]
- }),
- },
- Tool {
- name: "run_task".to_string(),
- description: "Start executing a pending task on an available daemon. The task must be in 'pending' or 'paused' status.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "task_id": {
- "type": "string",
- "description": "ID of the task to run"
- },
- "daemon_id": {
- "type": "string",
- "description": "Optional specific daemon ID to run on. If not specified, an available daemon will be selected."
- }
- },
- "required": ["task_id"]
- }),
- },
- Tool {
- name: "pause_task".to_string(),
- description: "Pause a running task. The container state will be preserved.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "task_id": {
- "type": "string",
- "description": "ID of the task to pause"
- }
- },
- "required": ["task_id"]
- }),
- },
- Tool {
- name: "resume_task".to_string(),
- description: "Resume a paused task.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "task_id": {
- "type": "string",
- "description": "ID of the task to resume"
- }
- },
- "required": ["task_id"]
- }),
- },
- Tool {
- name: "interrupt_task".to_string(),
- description: "Interrupt a running task. Use graceful=true to allow current operation to complete.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "task_id": {
- "type": "string",
- "description": "ID of the task to interrupt"
- },
- "graceful": {
- "type": "boolean",
- "description": "If true, wait for current operation to complete before stopping"
- }
- },
- "required": ["task_id"]
- }),
- },
- Tool {
- name: "discard_task".to_string(),
- description: "Discard a task and delete its overlay. All changes will be lost. Use with caution.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "task_id": {
- "type": "string",
- "description": "ID of the task to discard"
- },
- "confirm": {
- "type": "boolean",
- "description": "Must be true to confirm deletion"
- }
- },
- "required": ["task_id", "confirm"]
- }),
- },
- // =============================================================================
- // Task Query Tools
- // =============================================================================
- Tool {
- name: "query_task_status".to_string(),
- description: "Get detailed status and information about a task.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "task_id": {
- "type": "string",
- "description": "ID of the task to query"
- }
- },
- "required": ["task_id"]
- }),
- },
- Tool {
- name: "list_tasks".to_string(),
- description: "List all tasks, optionally filtered by status or parent.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "status_filter": {
- "type": "string",
- "enum": ["pending", "running", "paused", "blocked", "done", "failed", "merged"],
- "description": "Optional filter by task status"
- },
- "parent_task_id": {
- "type": "string",
- "description": "Optional filter to list only subtasks of this parent"
- }
- }
- }),
- },
- Tool {
- name: "list_subtasks".to_string(),
- description: "List all subtasks of a specific task.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "task_id": {
- "type": "string",
- "description": "ID of the parent task"
- }
- },
- "required": ["task_id"]
- }),
- },
- Tool {
- name: "list_siblings".to_string(),
- description: "List sibling tasks (tasks with the same parent) of a specific task.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "task_id": {
- "type": "string",
- "description": "ID of the task to find siblings for"
- }
- },
- "required": ["task_id"]
- }),
- },
- Tool {
- name: "list_daemons".to_string(),
- description: "List all connected daemons and their current status.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {}
- }),
- },
- Tool {
- name: "list_daemon_directories".to_string(),
- description: "List all available directories from connected daemons. Use this to find existing repositories and suggested working directories when creating tasks. Returns directories like the daemon's working directory and home directory where repos can be cloned.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {}
- }),
- },
- // =============================================================================
- // File Access Tools
- // =============================================================================
- Tool {
- name: "list_files".to_string(),
- description: "List all files available in the system. Returns file IDs, names, and descriptions.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {}
- }),
- },
- Tool {
- name: "read_file".to_string(),
- description: "Read the contents of a file from the files system. Returns the file's name, description, summary, body content (headings and paragraphs), and transcript entries with speaker and timing information.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "file_id": {
- "type": "string",
- "description": "ID of the file to read"
- }
- },
- "required": ["file_id"]
- }),
- },
- // =============================================================================
- // Task Communication Tools
- // =============================================================================
- Tool {
- name: "send_message_to_task".to_string(),
- description: "Send a message to a running task's Claude Code instance. Use this to provide additional context, answer questions, or give new instructions.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "task_id": {
- "type": "string",
- "description": "ID of the running task"
- },
- "message": {
- "type": "string",
- "description": "Message to send to the task"
- }
- },
- "required": ["task_id", "message"]
- }),
- },
- Tool {
- name: "update_task_plan".to_string(),
- description: "Update the plan/instructions for a task. Can optionally interrupt a running task to apply new plan.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "task_id": {
- "type": "string",
- "description": "ID of the task to update"
- },
- "new_plan": {
- "type": "string",
- "description": "New plan/instructions for the task"
- },
- "interrupt_if_running": {
- "type": "boolean",
- "description": "If true and task is running, interrupt it to apply new plan"
- }
- },
- "required": ["task_id", "new_plan"]
- }),
- },
- // =============================================================================
- // Overlay/Merge Tools
- // =============================================================================
- Tool {
- name: "peek_sibling_overlay".to_string(),
- description: "View the changes made by a sibling task's overlay. Useful for understanding what other tasks have done before merging.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "sibling_task_id": {
- "type": "string",
- "description": "ID of the sibling task to peek at"
- }
- },
- "required": ["sibling_task_id"]
- }),
- },
- Tool {
- name: "get_overlay_diff".to_string(),
- description: "Get a git diff of all changes in a task's overlay.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "task_id": {
- "type": "string",
- "description": "ID of the task"
- }
- },
- "required": ["task_id"]
- }),
- },
- Tool {
- name: "preview_merge".to_string(),
- description: "Preview what a merge would look like without actually merging. Shows potential conflicts.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "task_id": {
- "type": "string",
- "description": "ID of the task to preview merge for"
- }
- },
- "required": ["task_id"]
- }),
- },
- Tool {
- name: "merge_subtask".to_string(),
- description: "Merge a completed subtask's changes to its parent branch.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "task_id": {
- "type": "string",
- "description": "ID of the subtask to merge"
- }
- },
- "required": ["task_id"]
- }),
- },
- Tool {
- name: "complete_task".to_string(),
- description: "Mark a task as complete and trigger the merge flow based on merge_mode. For 'pr' mode, creates a pull request. For 'auto' mode, merges directly.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "task_id": {
- "type": "string",
- "description": "ID of the task to complete"
- }
- },
- "required": ["task_id"]
- }),
- },
- Tool {
- name: "set_merge_mode".to_string(),
- description: "Change the merge mode for a task.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "task_id": {
- "type": "string",
- "description": "ID of the task"
- },
- "mode": {
- "type": "string",
- "enum": ["pr", "auto", "manual"],
- "description": "New merge mode: 'pr' (create PR), 'auto' (auto-merge), 'manual' (leave for manual review)"
- }
- },
- "required": ["task_id", "mode"]
- }),
- },
- // =============================================================================
- // Interactive Tools
- // =============================================================================
- Tool {
- name: "ask_user".to_string(),
- description: "Ask the user one or more questions. Use this when you need clarification, want to offer choices, or need user input before proceeding. Questions can be single-select (user picks one option) or multi-select (user can pick multiple options). The question text supports markdown formatting.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "questions": {
- "type": "array",
- "description": "List of questions to ask the user",
- "items": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string",
- "description": "Unique identifier for this question"
- },
- "question": {
- "type": "string",
- "description": "The question to ask the user. Supports markdown formatting (bold, code, lists, etc.)"
- },
- "options": {
- "type": "array",
- "items": { "type": "string" },
- "description": "Multiple choice options for the user to select from"
- },
- "allowMultiple": {
- "type": "boolean",
- "description": "If true, user can select multiple options (multi-select). If false or omitted, user selects exactly one option (single-select). Default: false"
- },
- "allowCustom": {
- "type": "boolean",
- "description": "If true, user can provide a custom text answer instead of selecting from options. Default: true"
- }
- },
- "required": ["id", "question", "options"]
- }
- }
- },
- "required": ["questions"]
- }),
- },
- // =============================================================================
- // Supervisor Tools (only available to supervisor tasks)
- // =============================================================================
- Tool {
- name: "get_all_contract_tasks".to_string(),
- description: "Get status of all tasks in the contract tree. Only available to supervisor tasks.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "contract_id": {
- "type": "string",
- "description": "ID of the contract to query tasks for"
- }
- },
- "required": ["contract_id"]
- }),
- },
- Tool {
- name: "wait_for_task_completion".to_string(),
- description: "Block until a task completes or timeout. Only available to supervisor tasks.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "task_id": {
- "type": "string",
- "description": "ID of the task to wait for"
- },
- "timeout_seconds": {
- "type": "integer",
- "description": "Maximum time to wait in seconds (default: 300)",
- "default": 300
- }
- },
- "required": ["task_id"]
- }),
- },
- Tool {
- name: "read_task_worktree".to_string(),
- description: "Read a file from any task's worktree. Only available to supervisor tasks.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "task_id": {
- "type": "string",
- "description": "ID of the task whose worktree to read from"
- },
- "file_path": {
- "type": "string",
- "description": "Path to the file within the worktree"
- }
- },
- "required": ["task_id", "file_path"]
- }),
- },
- Tool {
- name: "spawn_task".to_string(),
- description: "Create and start a child task (fire and forget). Only available to supervisor tasks.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "description": "Name of the task"
- },
- "plan": {
- "type": "string",
- "description": "Detailed instructions/plan for what the task should accomplish"
- },
- "parent_task_id": {
- "type": "string",
- "description": "Optional parent task to branch from"
- },
- "checkpoint_sha": {
- "type": "string",
- "description": "Optional checkpoint SHA to branch from"
- },
- "repository_url": {
- "type": "string",
- "description": "Git repository URL (optional - inherits from contract if not provided)"
- },
- "base_branch": {
- "type": "string",
- "description": "Optional base branch to start from"
- }
- },
- "required": ["name", "plan"]
- }),
- },
- Tool {
- name: "create_checkpoint".to_string(),
- description: "Create a git checkpoint (commit) in the current task's worktree. Only available to supervisor tasks.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "task_id": {
- "type": "string",
- "description": "ID of the task to checkpoint"
- },
- "message": {
- "type": "string",
- "description": "Commit message for the checkpoint"
- }
- },
- "required": ["task_id", "message"]
- }),
- },
- Tool {
- name: "list_task_checkpoints".to_string(),
- description: "List all checkpoints for a task.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "task_id": {
- "type": "string",
- "description": "ID of the task to list checkpoints for"
- }
- },
- "required": ["task_id"]
- }),
- },
- Tool {
- name: "get_task_tree".to_string(),
- description: "Get the full task tree starting from a specific task.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "task_id": {
- "type": "string",
- "description": "ID of the root task"
- }
- },
- "required": ["task_id"]
- }),
- },
- ]
-});
-
-/// Request for mesh tool operations that require async database/daemon access
-#[derive(Debug, Clone)]
-pub enum MeshToolRequest {
- // Task lifecycle
- CreateTask {
- name: String,
- plan: String,
- parent_task_id: Option<Uuid>,
- repository_url: Option<String>,
- base_branch: Option<String>,
- merge_mode: Option<String>,
- priority: Option<i32>,
- },
- RunTask {
- task_id: Uuid,
- daemon_id: Option<Uuid>,
- },
- PauseTask {
- task_id: Uuid,
- },
- ResumeTask {
- task_id: Uuid,
- },
- InterruptTask {
- task_id: Uuid,
- graceful: bool,
- },
- DiscardTask {
- task_id: Uuid,
- },
-
- // Task queries
- QueryTaskStatus {
- task_id: Uuid,
- },
- ListTasks {
- status_filter: Option<String>,
- parent_task_id: Option<Uuid>,
- },
- ListSubtasks {
- task_id: Uuid,
- },
- ListSiblings {
- task_id: Uuid,
- },
- ListDaemons,
- ListDaemonDirectories,
-
- // File access
- ListFiles,
- ReadFile {
- file_id: Uuid,
- },
-
- // Task communication
- SendMessageToTask {
- task_id: Uuid,
- message: String,
- },
- UpdateTaskPlan {
- task_id: Uuid,
- new_plan: String,
- interrupt_if_running: bool,
- },
-
- // Overlay/merge operations
- PeekSiblingOverlay {
- sibling_task_id: Uuid,
- },
- GetOverlayDiff {
- task_id: Uuid,
- },
- PreviewMerge {
- task_id: Uuid,
- },
- MergeSubtask {
- task_id: Uuid,
- },
- CompleteTask {
- task_id: Uuid,
- },
- SetMergeMode {
- task_id: Uuid,
- mode: String,
- },
-
- // Supervisor tools (only for supervisor tasks)
- GetAllContractTasks {
- contract_id: Uuid,
- },
- WaitForTaskCompletion {
- task_id: Uuid,
- timeout_seconds: i32,
- },
- ReadTaskWorktree {
- task_id: Uuid,
- file_path: String,
- },
- SpawnTask {
- name: String,
- plan: String,
- parent_task_id: Option<Uuid>,
- checkpoint_sha: Option<String>,
- repository_url: Option<String>,
- base_branch: Option<String>,
- },
- CreateCheckpoint {
- task_id: Uuid,
- message: String,
- },
- ListTaskCheckpoints {
- task_id: Uuid,
- },
- GetTaskTree {
- task_id: Uuid,
- },
-}
-
-/// Result from executing a mesh tool
-#[derive(Debug)]
-pub struct MeshToolExecutionResult {
- pub success: bool,
- pub message: String,
- pub data: Option<serde_json::Value>,
- /// Request for async operations (handled by mesh_chat handler)
- pub request: Option<MeshToolRequest>,
- /// Questions to ask the user (pauses conversation)
- pub pending_questions: Option<Vec<super::tools::UserQuestion>>,
-}
-
-/// Parse and validate a mesh tool call, returning a MeshToolRequest for async handling
-pub fn parse_mesh_tool_call(
- call: &super::tools::ToolCall,
-) -> MeshToolExecutionResult {
- match call.name.as_str() {
- // Task lifecycle
- "create_task" => parse_create_task(call),
- "run_task" => parse_run_task(call),
- "pause_task" => parse_pause_task(call),
- "resume_task" => parse_resume_task(call),
- "interrupt_task" => parse_interrupt_task(call),
- "discard_task" => parse_discard_task(call),
-
- // Task queries
- "query_task_status" => parse_query_task_status(call),
- "list_tasks" => parse_list_tasks(call),
- "list_subtasks" => parse_list_subtasks(call),
- "list_siblings" => parse_list_siblings(call),
- "list_daemons" => parse_list_daemons(),
- "list_daemon_directories" => parse_list_daemon_directories(),
-
- // File access
- "list_files" => parse_list_files(),
- "read_file" => parse_read_file(call),
-
- // Task communication
- "send_message_to_task" => parse_send_message_to_task(call),
- "update_task_plan" => parse_update_task_plan(call),
-
- // Overlay/merge operations
- "peek_sibling_overlay" => parse_peek_sibling_overlay(call),
- "get_overlay_diff" => parse_get_overlay_diff(call),
- "preview_merge" => parse_preview_merge(call),
- "merge_subtask" => parse_merge_subtask(call),
- "complete_task" => parse_complete_task(call),
- "set_merge_mode" => parse_set_merge_mode(call),
-
- // Interactive tools
- "ask_user" => parse_ask_user(call),
-
- // Supervisor tools
- "get_all_contract_tasks" => parse_get_all_contract_tasks(call),
- "wait_for_task_completion" => parse_wait_for_task_completion(call),
- "read_task_worktree" => parse_read_task_worktree(call),
- "spawn_task" => parse_spawn_task(call),
- "create_checkpoint" => parse_create_checkpoint(call),
- "list_task_checkpoints" => parse_list_task_checkpoints(call),
- "get_task_tree" => parse_get_task_tree(call),
-
- _ => MeshToolExecutionResult {
- success: false,
- message: format!("Unknown mesh tool: {}", call.name),
- data: None,
- request: None,
- pending_questions: None,
- },
- }
-}
-
-// =============================================================================
-// Tool Parsing Functions
-// =============================================================================
-
-fn parse_create_task(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let name = call.arguments.get("name").and_then(|v| v.as_str());
- let plan = call.arguments.get("plan").and_then(|v| v.as_str());
- let repository_url = call
- .arguments
- .get("repository_url")
- .and_then(|v| v.as_str());
-
- let Some(name) = name else {
- return error_result("Missing required parameter: name");
- };
- let Some(plan) = plan else {
- return error_result("Missing required parameter: plan");
- };
- let Some(repository_url) = repository_url else {
- return error_result("Missing required parameter: repository_url");
- };
-
- let parent_task_id = call
- .arguments
- .get("parent_task_id")
- .and_then(|v| v.as_str())
- .and_then(|s| Uuid::parse_str(s).ok());
-
- let repository_url = Some(repository_url.to_string());
-
- let base_branch = call
- .arguments
- .get("base_branch")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string());
-
- let merge_mode = call
- .arguments
- .get("merge_mode")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string());
-
- let priority = call
- .arguments
- .get("priority")
- .and_then(|v| v.as_i64())
- .map(|v| v as i32);
-
- MeshToolExecutionResult {
- success: true,
- message: "Creating task...".to_string(),
- data: None,
- request: Some(MeshToolRequest::CreateTask {
- name: name.to_string(),
- plan: plan.to_string(),
- parent_task_id,
- repository_url,
- base_branch,
- merge_mode,
- priority,
- }),
- pending_questions: None,
- }
-}
-
-fn parse_run_task(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let task_id = parse_uuid_arg(call, "task_id");
- let Some(task_id) = task_id else {
- return error_result("Missing or invalid required parameter: task_id");
- };
-
- let daemon_id = call
- .arguments
- .get("daemon_id")
- .and_then(|v| v.as_str())
- .and_then(|s| Uuid::parse_str(s).ok());
-
- MeshToolExecutionResult {
- success: true,
- message: "Starting task...".to_string(),
- data: None,
- request: Some(MeshToolRequest::RunTask { task_id, daemon_id }),
- pending_questions: None,
- }
-}
-
-fn parse_pause_task(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let task_id = parse_uuid_arg(call, "task_id");
- let Some(task_id) = task_id else {
- return error_result("Missing or invalid required parameter: task_id");
- };
-
- MeshToolExecutionResult {
- success: true,
- message: "Pausing task...".to_string(),
- data: None,
- request: Some(MeshToolRequest::PauseTask { task_id }),
- pending_questions: None,
- }
-}
-
-fn parse_resume_task(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let task_id = parse_uuid_arg(call, "task_id");
- let Some(task_id) = task_id else {
- return error_result("Missing or invalid required parameter: task_id");
- };
-
- MeshToolExecutionResult {
- success: true,
- message: "Resuming task...".to_string(),
- data: None,
- request: Some(MeshToolRequest::ResumeTask { task_id }),
- pending_questions: None,
- }
-}
-
-fn parse_interrupt_task(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let task_id = parse_uuid_arg(call, "task_id");
- let Some(task_id) = task_id else {
- return error_result("Missing or invalid required parameter: task_id");
- };
-
- let graceful = call
- .arguments
- .get("graceful")
- .and_then(|v| v.as_bool())
- .unwrap_or(true);
-
- MeshToolExecutionResult {
- success: true,
- message: if graceful {
- "Gracefully interrupting task...".to_string()
- } else {
- "Force interrupting task...".to_string()
- },
- data: None,
- request: Some(MeshToolRequest::InterruptTask { task_id, graceful }),
- pending_questions: None,
- }
-}
-
-fn parse_discard_task(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let task_id = parse_uuid_arg(call, "task_id");
- let Some(task_id) = task_id else {
- return error_result("Missing or invalid required parameter: task_id");
- };
-
- let confirm = call
- .arguments
- .get("confirm")
- .and_then(|v| v.as_bool())
- .unwrap_or(false);
-
- if !confirm {
- return error_result("Must set confirm=true to discard a task");
- }
-
- MeshToolExecutionResult {
- success: true,
- message: "Discarding task...".to_string(),
- data: None,
- request: Some(MeshToolRequest::DiscardTask { task_id }),
- pending_questions: None,
- }
-}
-
-fn parse_query_task_status(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let task_id = parse_uuid_arg(call, "task_id");
- let Some(task_id) = task_id else {
- return error_result("Missing or invalid required parameter: task_id");
- };
-
- MeshToolExecutionResult {
- success: true,
- message: "Querying task status...".to_string(),
- data: None,
- request: Some(MeshToolRequest::QueryTaskStatus { task_id }),
- pending_questions: None,
- }
-}
-
-fn parse_list_tasks(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let status_filter = call
- .arguments
- .get("status_filter")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string());
-
- let parent_task_id = call
- .arguments
- .get("parent_task_id")
- .and_then(|v| v.as_str())
- .and_then(|s| Uuid::parse_str(s).ok());
-
- MeshToolExecutionResult {
- success: true,
- message: "Listing tasks...".to_string(),
- data: None,
- request: Some(MeshToolRequest::ListTasks {
- status_filter,
- parent_task_id,
- }),
- pending_questions: None,
- }
-}
-
-fn parse_list_subtasks(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let task_id = parse_uuid_arg(call, "task_id");
- let Some(task_id) = task_id else {
- return error_result("Missing or invalid required parameter: task_id");
- };
-
- MeshToolExecutionResult {
- success: true,
- message: "Listing subtasks...".to_string(),
- data: None,
- request: Some(MeshToolRequest::ListSubtasks { task_id }),
- pending_questions: None,
- }
-}
-
-fn parse_list_siblings(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let task_id = parse_uuid_arg(call, "task_id");
- let Some(task_id) = task_id else {
- return error_result("Missing or invalid required parameter: task_id");
- };
-
- MeshToolExecutionResult {
- success: true,
- message: "Listing sibling tasks...".to_string(),
- data: None,
- request: Some(MeshToolRequest::ListSiblings { task_id }),
- pending_questions: None,
- }
-}
-
-fn parse_list_daemons() -> MeshToolExecutionResult {
- MeshToolExecutionResult {
- success: true,
- message: "Listing daemons...".to_string(),
- data: None,
- request: Some(MeshToolRequest::ListDaemons),
- pending_questions: None,
- }
-}
-
-fn parse_list_daemon_directories() -> MeshToolExecutionResult {
- MeshToolExecutionResult {
- success: true,
- message: "Listing daemon directories...".to_string(),
- data: None,
- request: Some(MeshToolRequest::ListDaemonDirectories),
- pending_questions: None,
- }
-}
-
-fn parse_list_files() -> MeshToolExecutionResult {
- MeshToolExecutionResult {
- success: true,
- message: "Listing files...".to_string(),
- data: None,
- request: Some(MeshToolRequest::ListFiles),
- pending_questions: None,
- }
-}
-
-fn parse_read_file(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let file_id = parse_uuid_arg(call, "file_id");
- let Some(file_id) = file_id else {
- return error_result("Missing or invalid required parameter: file_id");
- };
-
- MeshToolExecutionResult {
- success: true,
- message: "Reading file...".to_string(),
- data: None,
- request: Some(MeshToolRequest::ReadFile { file_id }),
- pending_questions: None,
- }
-}
-
-fn parse_send_message_to_task(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let task_id = parse_uuid_arg(call, "task_id");
- let Some(task_id) = task_id else {
- return error_result("Missing or invalid required parameter: task_id");
- };
-
- let message = call.arguments.get("message").and_then(|v| v.as_str());
- let Some(message) = message else {
- return error_result("Missing required parameter: message");
- };
-
- MeshToolExecutionResult {
- success: true,
- message: "Sending message to task...".to_string(),
- data: None,
- request: Some(MeshToolRequest::SendMessageToTask {
- task_id,
- message: message.to_string(),
- }),
- pending_questions: None,
- }
-}
-
-fn parse_update_task_plan(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let task_id = parse_uuid_arg(call, "task_id");
- let Some(task_id) = task_id else {
- return error_result("Missing or invalid required parameter: task_id");
- };
-
- let new_plan = call.arguments.get("new_plan").and_then(|v| v.as_str());
- let Some(new_plan) = new_plan else {
- return error_result("Missing required parameter: new_plan");
- };
-
- let interrupt_if_running = call
- .arguments
- .get("interrupt_if_running")
- .and_then(|v| v.as_bool())
- .unwrap_or(false);
-
- MeshToolExecutionResult {
- success: true,
- message: "Updating task plan...".to_string(),
- data: None,
- request: Some(MeshToolRequest::UpdateTaskPlan {
- task_id,
- new_plan: new_plan.to_string(),
- interrupt_if_running,
- }),
- pending_questions: None,
- }
-}
-
-fn parse_peek_sibling_overlay(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let sibling_task_id = parse_uuid_arg(call, "sibling_task_id");
- let Some(sibling_task_id) = sibling_task_id else {
- return error_result("Missing or invalid required parameter: sibling_task_id");
- };
-
- MeshToolExecutionResult {
- success: true,
- message: "Peeking at sibling overlay...".to_string(),
- data: None,
- request: Some(MeshToolRequest::PeekSiblingOverlay { sibling_task_id }),
- pending_questions: None,
- }
-}
-
-fn parse_get_overlay_diff(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let task_id = parse_uuid_arg(call, "task_id");
- let Some(task_id) = task_id else {
- return error_result("Missing or invalid required parameter: task_id");
- };
-
- MeshToolExecutionResult {
- success: true,
- message: "Getting overlay diff...".to_string(),
- data: None,
- request: Some(MeshToolRequest::GetOverlayDiff { task_id }),
- pending_questions: None,
- }
-}
-
-fn parse_preview_merge(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let task_id = parse_uuid_arg(call, "task_id");
- let Some(task_id) = task_id else {
- return error_result("Missing or invalid required parameter: task_id");
- };
-
- MeshToolExecutionResult {
- success: true,
- message: "Previewing merge...".to_string(),
- data: None,
- request: Some(MeshToolRequest::PreviewMerge { task_id }),
- pending_questions: None,
- }
-}
-
-fn parse_merge_subtask(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let task_id = parse_uuid_arg(call, "task_id");
- let Some(task_id) = task_id else {
- return error_result("Missing or invalid required parameter: task_id");
- };
-
- MeshToolExecutionResult {
- success: true,
- message: "Merging subtask...".to_string(),
- data: None,
- request: Some(MeshToolRequest::MergeSubtask { task_id }),
- pending_questions: None,
- }
-}
-
-fn parse_complete_task(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let task_id = parse_uuid_arg(call, "task_id");
- let Some(task_id) = task_id else {
- return error_result("Missing or invalid required parameter: task_id");
- };
-
- MeshToolExecutionResult {
- success: true,
- message: "Completing task...".to_string(),
- data: None,
- request: Some(MeshToolRequest::CompleteTask { task_id }),
- pending_questions: None,
- }
-}
-
-fn parse_set_merge_mode(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let task_id = parse_uuid_arg(call, "task_id");
- let Some(task_id) = task_id else {
- return error_result("Missing or invalid required parameter: task_id");
- };
-
- let mode = call.arguments.get("mode").and_then(|v| v.as_str());
- let Some(mode) = mode else {
- return error_result("Missing required parameter: mode");
- };
-
- if !["pr", "auto", "manual"].contains(&mode) {
- return error_result("Invalid mode. Must be 'pr', 'auto', or 'manual'");
- }
-
- MeshToolExecutionResult {
- success: true,
- message: format!("Setting merge mode to '{}'...", mode),
- data: None,
- request: Some(MeshToolRequest::SetMergeMode {
- task_id,
- mode: mode.to_string(),
- }),
- pending_questions: None,
- }
-}
-
-fn parse_ask_user(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let questions_value = call.arguments.get("questions");
-
- let Some(questions_array) = questions_value.and_then(|v| v.as_array()) else {
- return error_result("Missing or invalid 'questions' parameter");
- };
-
- let mut questions: Vec<super::tools::UserQuestion> = Vec::new();
-
- for q in questions_array {
- let id = q.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();
- let question = q.get("question").and_then(|v| v.as_str()).unwrap_or("").to_string();
- let options: Vec<String> = q
- .get("options")
- .and_then(|v| v.as_array())
- .map(|arr| {
- arr.iter()
- .filter_map(|o| o.as_str())
- .map(|s| s.to_string())
- .collect()
- })
- .unwrap_or_default();
- let allow_multiple = q.get("allowMultiple").and_then(|v| v.as_bool()).unwrap_or(false);
- let allow_custom = q.get("allowCustom").and_then(|v| v.as_bool()).unwrap_or(true);
-
- if id.is_empty() || question.is_empty() || options.is_empty() {
- continue;
- }
-
- questions.push(super::tools::UserQuestion {
- id,
- question,
- options,
- allow_multiple,
- allow_custom,
- });
- }
-
- if questions.is_empty() {
- return error_result("No valid questions provided");
- }
-
- let question_count = questions.len();
- MeshToolExecutionResult {
- success: true,
- message: format!("Asking user {} question(s). Waiting for response...", question_count),
- data: None,
- request: None,
- pending_questions: Some(questions),
- }
-}
-
-// =============================================================================
-// Supervisor Tool Parsing Functions
-// =============================================================================
-
-fn parse_get_all_contract_tasks(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let Some(contract_id) = parse_uuid_arg(call, "contract_id") else {
- return error_result("Missing or invalid contract_id");
- };
-
- MeshToolExecutionResult {
- success: true,
- message: "Querying all contract tasks...".to_string(),
- data: None,
- request: Some(MeshToolRequest::GetAllContractTasks { contract_id }),
- pending_questions: None,
- }
-}
-
-fn parse_wait_for_task_completion(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let Some(task_id) = parse_uuid_arg(call, "task_id") else {
- return error_result("Missing or invalid task_id");
- };
-
- let timeout_seconds = call
- .arguments
- .get("timeout_seconds")
- .and_then(|v| v.as_i64())
- .map(|v| v as i32)
- .unwrap_or(300);
-
- MeshToolExecutionResult {
- success: true,
- message: format!("Waiting for task completion (timeout: {}s)...", timeout_seconds),
- data: None,
- request: Some(MeshToolRequest::WaitForTaskCompletion {
- task_id,
- timeout_seconds,
- }),
- pending_questions: None,
- }
-}
-
-fn parse_read_task_worktree(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let Some(task_id) = parse_uuid_arg(call, "task_id") else {
- return error_result("Missing or invalid task_id");
- };
-
- let Some(file_path) = call.arguments.get("file_path").and_then(|v| v.as_str()) else {
- return error_result("Missing required parameter: file_path");
- };
-
- MeshToolExecutionResult {
- success: true,
- message: format!("Reading file from task worktree: {}", file_path),
- data: None,
- request: Some(MeshToolRequest::ReadTaskWorktree {
- task_id,
- file_path: file_path.to_string(),
- }),
- pending_questions: None,
- }
-}
-
-fn parse_spawn_task(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let Some(name) = call.arguments.get("name").and_then(|v| v.as_str()) else {
- return error_result("Missing required parameter: name");
- };
-
- let Some(plan) = call.arguments.get("plan").and_then(|v| v.as_str()) else {
- return error_result("Missing required parameter: plan");
- };
-
- let parent_task_id = parse_uuid_arg(call, "parent_task_id");
-
- let checkpoint_sha = call
- .arguments
- .get("checkpoint_sha")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string());
-
- let repository_url = call
- .arguments
- .get("repository_url")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string());
-
- let base_branch = call
- .arguments
- .get("base_branch")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string());
-
- MeshToolExecutionResult {
- success: true,
- message: format!("Spawning task: {}", name),
- data: None,
- request: Some(MeshToolRequest::SpawnTask {
- name: name.to_string(),
- plan: plan.to_string(),
- parent_task_id,
- checkpoint_sha,
- repository_url,
- base_branch,
- }),
- pending_questions: None,
- }
-}
-
-fn parse_create_checkpoint(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let Some(task_id) = parse_uuid_arg(call, "task_id") else {
- return error_result("Missing or invalid task_id");
- };
-
- let Some(message) = call.arguments.get("message").and_then(|v| v.as_str()) else {
- return error_result("Missing required parameter: message");
- };
-
- MeshToolExecutionResult {
- success: true,
- message: format!("Creating checkpoint: {}", message),
- data: None,
- request: Some(MeshToolRequest::CreateCheckpoint {
- task_id,
- message: message.to_string(),
- }),
- pending_questions: None,
- }
-}
-
-fn parse_list_task_checkpoints(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let Some(task_id) = parse_uuid_arg(call, "task_id") else {
- return error_result("Missing or invalid task_id");
- };
-
- MeshToolExecutionResult {
- success: true,
- message: "Listing task checkpoints...".to_string(),
- data: None,
- request: Some(MeshToolRequest::ListTaskCheckpoints { task_id }),
- pending_questions: None,
- }
-}
-
-fn parse_get_task_tree(call: &super::tools::ToolCall) -> MeshToolExecutionResult {
- let Some(task_id) = parse_uuid_arg(call, "task_id") else {
- return error_result("Missing or invalid task_id");
- };
-
- MeshToolExecutionResult {
- success: true,
- message: "Getting task tree...".to_string(),
- data: None,
- request: Some(MeshToolRequest::GetTaskTree { task_id }),
- pending_questions: None,
- }
-}
-
-// =============================================================================
-// Helper Functions
-// =============================================================================
-
-fn parse_uuid_arg(call: &super::tools::ToolCall, key: &str) -> Option<Uuid> {
- call.arguments
- .get(key)
- .and_then(|v| v.as_str())
- .and_then(|s| Uuid::parse_str(s).ok())
-}
-
-fn error_result(message: &str) -> MeshToolExecutionResult {
- MeshToolExecutionResult {
- success: false,
- message: message.to_string(),
- data: None,
- request: None,
- pending_questions: None,
- }
-}
diff --git a/makima/src/llm/mod.rs b/makima/src/llm/mod.rs
deleted file mode 100644
index 6c9965c..0000000
--- a/makima/src/llm/mod.rs
+++ /dev/null
@@ -1,73 +0,0 @@
-//! LLM integration module for file editing via tool calling.
-
-pub mod claude;
-pub mod contract_evaluator;
-pub mod contract_tools;
-pub mod discuss_tools;
-pub mod groq;
-pub mod markdown;
-pub mod mesh_tools;
-pub mod phase_guidance;
-pub mod task_output;
-pub mod templates;
-pub mod tools;
-pub mod transcript_analyzer;
-
-pub use claude::{ClaudeClient, ClaudeModel};
-pub use contract_tools::{
- parse_contract_tool_call, ChainedTaskDef, ContractToolExecutionResult, ContractToolRequest,
- CONTRACT_TOOLS,
-};
-pub use discuss_tools::{
- parse_discuss_tool_call, DiscussToolExecutionResult, DiscussToolRequest, DISCUSS_TOOLS,
-};
-pub use groq::GroqClient;
-pub use mesh_tools::{parse_mesh_tool_call, MeshToolExecutionResult, MeshToolRequest, MESH_TOOLS};
-pub use phase_guidance::{
- check_deliverables_met, format_checklist_markdown, generate_deliverable_prompt_guidance,
- get_next_phase_for_contract, get_phase_checklist_for_type, get_phase_deliverables,
- get_phase_deliverables_for_type, get_phase_deliverables_with_config, should_auto_progress,
- AutoProgressAction, AutoProgressDecision, Deliverable, DeliverableCheckResult, DeliverableItem,
- DeliverablePriority, DeliverableStatus, PhaseChecklist, PhaseDeliverables, TaskInfo, TaskStats,
-};
-pub use task_output::{
- analyze_task_output, format_parsed_tasks, parse_tasks_from_breakdown, ParsedTask,
- PhaseImpact, SuggestedAction, TaskOutputAnalysis, TaskParseResult,
-};
-pub use markdown::{body_to_markdown, markdown_to_body};
-pub use templates::{all_contract_types, ContractTypeTemplate};
-pub use tools::{
- execute_tool_call, Tool, ToolCall, ToolResult, UserAnswer, UserQuestion, VersionToolRequest,
- AVAILABLE_TOOLS,
-};
-pub use transcript_analyzer::{
- TranscriptAnalysisResult, ExtractedRequirement, ExtractedDecision,
- ExtractedActionItem, SpeakerStats, format_transcript_for_analysis,
- calculate_speaker_stats, build_analysis_prompt, parse_analysis_response,
-};
-pub use contract_evaluator::{
- ContractEvaluator, ContractEvaluationResult, ContractEvaluatorError,
-};
-
-/// Available LLM providers and models
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
-pub enum LlmModel {
- /// Claude Sonnet 4.5 - balanced speed and capability
- ClaudeSonnet,
- /// Claude Opus 4.5 (default) - most capable
- #[default]
- ClaudeOpus,
- /// Groq Kimi - fast alternative provider
- GroqKimi,
-}
-
-impl LlmModel {
- pub fn from_str(s: &str) -> Option<Self> {
- match s.to_lowercase().as_str() {
- "claude-sonnet" | "sonnet" | "claude" => Some(LlmModel::ClaudeSonnet),
- "claude-opus" | "opus" => Some(LlmModel::ClaudeOpus),
- "groq" | "kimi" | "groq-kimi" => Some(LlmModel::GroqKimi),
- _ => None,
- }
- }
-}
diff --git a/makima/src/llm/phase_guidance.rs b/makima/src/llm/phase_guidance.rs
deleted file mode 100644
index 712e8bb..0000000
--- a/makima/src/llm/phase_guidance.rs
+++ /dev/null
@@ -1,1032 +0,0 @@
-//! Phase guidance and deliverables tracking for contract management.
-//!
-//! This module provides structured guidance for each contract phase, tracking
-//! expected deliverables and completion criteria.
-//!
-//! ## Contract Types
-//!
-//! ### Simple
-//! - **Plan phase**: One required deliverable: "Plan"
-//! - **Execute phase**: One required deliverable: "PR"
-//!
-//! ### Specification
-//! - **Research phase**: One required deliverable: "Research Notes"
-//! - **Specify phase**: One required deliverable: "Requirements Document"
-//! - **Plan phase**: One required deliverable: "Plan"
-//! - **Execute phase**: One required deliverable: "PR"
-//! - **Review phase**: One required deliverable: "Release Notes"
-//!
-//! ### Execute
-//! - **Execute phase only**: No deliverables at all
-
-use serde::{Deserialize, Serialize};
-use utoipa::ToSchema;
-
-/// Priority level for deliverables
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
-#[serde(rename_all = "lowercase")]
-pub enum DeliverablePriority {
- /// Must be completed before advancing phase
- Required,
- /// Strongly suggested for phase completion
- Recommended,
- /// Nice to have, not blocking
- Optional,
-}
-
-/// A deliverable for a phase
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
-pub struct Deliverable {
- /// Unique identifier for the deliverable
- pub id: String,
- /// Display name
- pub name: String,
- /// Priority level
- pub priority: DeliverablePriority,
- /// Brief description of purpose
- pub description: String,
-}
-
-/// Expected deliverables for a phase
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
-pub struct PhaseDeliverables {
- /// Phase name
- pub phase: String,
- /// Deliverables for this phase
- pub deliverables: Vec<Deliverable>,
- /// Whether a repository is required for this phase
- pub requires_repository: bool,
- /// Whether tasks should be completed in this phase
- pub requires_tasks: bool,
- /// Guidance text for this phase
- pub guidance: String,
-}
-
-/// Status of a deliverable
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
-pub struct DeliverableStatus {
- /// Deliverable ID
- pub id: String,
- /// Display name
- pub name: String,
- /// Priority
- pub priority: DeliverablePriority,
- /// Whether it has been completed
- pub completed: bool,
-}
-
-/// Checklist for phase completion
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
-pub struct PhaseChecklist {
- /// Current phase
- pub phase: String,
- /// Deliverable status list
- pub deliverables: Vec<DeliverableStatus>,
- /// Whether repository is configured
- pub has_repository: bool,
- /// Whether repository was required
- pub repository_required: bool,
- /// Task statistics (for execute phase)
- pub task_stats: Option<TaskStats>,
- /// Overall completion percentage (0-100)
- pub completion_percentage: u8,
- /// Summary message
- pub summary: String,
- /// Suggestions for next actions
- pub suggestions: Vec<String>,
-}
-
-/// Task statistics for execute phase
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
-pub struct TaskStats {
- pub total: usize,
- pub pending: usize,
- pub running: usize,
- pub done: usize,
- pub failed: usize,
-}
-
-/// Minimal task info for checklist building
-pub struct TaskInfo {
- pub name: String,
- pub status: String,
-}
-
-use crate::db::models::PhaseConfig;
-
-/// Get phase deliverables configuration (legacy, defaults to "simple" contract type)
-pub fn get_phase_deliverables(phase: &str) -> PhaseDeliverables {
- get_phase_deliverables_for_type(phase, "simple")
-}
-
-/// Get phase deliverables configuration for a specific contract type
-pub fn get_phase_deliverables_for_type(phase: &str, contract_type: &str) -> PhaseDeliverables {
- match contract_type {
- "execute" => get_execute_type_deliverables(phase),
- "specification" => get_specification_type_deliverables(phase),
- "simple" | _ => get_simple_type_deliverables(phase),
- }
-}
-
-/// Get phase deliverables from a custom PhaseConfig
-/// This is used for contracts with custom templates
-pub fn get_phase_deliverables_from_config(phase: &str, config: &PhaseConfig) -> PhaseDeliverables {
- // Check if this phase exists in the config
- let phase_exists = config.phases.iter().any(|p| p.id == phase);
- if !phase_exists {
- return PhaseDeliverables {
- phase: phase.to_string(),
- deliverables: vec![],
- requires_repository: false,
- requires_tasks: false,
- guidance: format!("Phase '{}' is not defined in this contract template", phase),
- };
- }
-
- // Get deliverables for this phase from the config
- let deliverables: Vec<Deliverable> = config
- .deliverables
- .get(phase)
- .map(|defs| {
- defs.iter()
- .map(|d| Deliverable {
- id: d.id.clone(),
- name: d.name.clone(),
- priority: match d.priority.as_str() {
- "recommended" => DeliverablePriority::Recommended,
- "optional" => DeliverablePriority::Optional,
- _ => DeliverablePriority::Required,
- },
- description: format!("{} deliverable", d.name),
- })
- .collect()
- })
- .unwrap_or_default();
-
- // Determine if repository is required (typically for execute-like phases)
- let requires_repository = phase == "execute" || phase == "plan";
-
- // Determine if tasks are required (typically for execute phase)
- let requires_tasks = phase == "execute";
-
- // Find the phase name for better guidance
- let phase_name = config
- .phases
- .iter()
- .find(|p| p.id == phase)
- .map(|p| p.name.clone())
- .unwrap_or_else(|| phase.to_string());
-
- let guidance = if deliverables.is_empty() {
- format!("Complete the {} phase. No specific deliverables are required.", phase_name)
- } else {
- let deliverable_names: Vec<_> = deliverables.iter().map(|d| d.name.clone()).collect();
- format!(
- "Complete the {} phase by producing the following deliverables: {}",
- phase_name,
- deliverable_names.join(", ")
- )
- };
-
- PhaseDeliverables {
- phase: phase.to_string(),
- deliverables,
- requires_repository,
- requires_tasks,
- guidance,
- }
-}
-
-/// Get phase deliverables, checking custom config first, then falling back to built-in types
-pub fn get_phase_deliverables_with_config(
- phase: &str,
- contract_type: &str,
- phase_config: Option<&PhaseConfig>,
-) -> PhaseDeliverables {
- // If we have a custom phase config, use it
- if let Some(config) = phase_config {
- return get_phase_deliverables_from_config(phase, config);
- }
-
- // Otherwise, fall back to built-in contract types
- get_phase_deliverables_for_type(phase, contract_type)
-}
-
-/// Get deliverables for 'simple' contract type
-fn get_simple_type_deliverables(phase: &str) -> PhaseDeliverables {
- match phase {
- "plan" => PhaseDeliverables {
- phase: "plan".to_string(),
- deliverables: vec![Deliverable {
- id: "plan-document".to_string(),
- name: "Plan".to_string(),
- priority: DeliverablePriority::Required,
- description: "Implementation plan detailing the approach and tasks".to_string(),
- }],
- requires_repository: true,
- requires_tasks: false,
- guidance: "Create a plan document that outlines the implementation approach. A repository must be configured before moving to Execute phase.".to_string(),
- },
- "execute" => PhaseDeliverables {
- phase: "execute".to_string(),
- deliverables: vec![Deliverable {
- id: "pull-request".to_string(),
- name: "Pull Request".to_string(),
- priority: DeliverablePriority::Required,
- description: "Pull request with the implemented changes".to_string(),
- }],
- requires_repository: true,
- requires_tasks: true,
- guidance: "Execute the plan and create a PR with the implemented changes. Complete all tasks to finish the contract.".to_string(),
- },
- _ => PhaseDeliverables {
- phase: phase.to_string(),
- deliverables: vec![],
- requires_repository: false,
- requires_tasks: false,
- guidance: "Unknown phase for simple contract type".to_string(),
- },
- }
-}
-
-/// Get deliverables for 'specification' contract type
-fn get_specification_type_deliverables(phase: &str) -> PhaseDeliverables {
- match phase {
- "research" => PhaseDeliverables {
- phase: "research".to_string(),
- deliverables: vec![Deliverable {
- id: "research-notes".to_string(),
- name: "Research Notes".to_string(),
- priority: DeliverablePriority::Required,
- description: "Document findings and insights during research".to_string(),
- }],
- requires_repository: false,
- requires_tasks: false,
- guidance: "Focus on understanding the problem space and document your findings in the Research Notes before moving to Specify phase.".to_string(),
- },
- "specify" => PhaseDeliverables {
- phase: "specify".to_string(),
- deliverables: vec![Deliverable {
- id: "requirements-document".to_string(),
- name: "Requirements Document".to_string(),
- priority: DeliverablePriority::Required,
- description: "Define functional and non-functional requirements".to_string(),
- }],
- requires_repository: false,
- requires_tasks: false,
- guidance: "Define what needs to be built with clear requirements in the Requirements Document. Ensure specifications are detailed enough for planning.".to_string(),
- },
- "plan" => PhaseDeliverables {
- phase: "plan".to_string(),
- deliverables: vec![Deliverable {
- id: "plan-document".to_string(),
- name: "Plan".to_string(),
- priority: DeliverablePriority::Required,
- description: "Implementation plan detailing the approach and tasks".to_string(),
- }],
- requires_repository: true,
- requires_tasks: false,
- guidance: "Create a plan document that outlines the implementation approach. A repository must be configured before moving to Execute phase.".to_string(),
- },
- "execute" => PhaseDeliverables {
- phase: "execute".to_string(),
- deliverables: vec![Deliverable {
- id: "pull-request".to_string(),
- name: "Pull Request".to_string(),
- priority: DeliverablePriority::Required,
- description: "Pull request with the implemented changes".to_string(),
- }],
- requires_repository: true,
- requires_tasks: true,
- guidance: "Execute the plan and create a PR with the implemented changes. Complete all tasks before moving to Review phase.".to_string(),
- },
- "review" => PhaseDeliverables {
- phase: "review".to_string(),
- deliverables: vec![Deliverable {
- id: "release-notes".to_string(),
- name: "Release Notes".to_string(),
- priority: DeliverablePriority::Required,
- description: "Document changes for release communication".to_string(),
- }],
- requires_repository: false,
- requires_tasks: false,
- guidance: "Review completed work and document the release in the Release Notes. The contract can be completed after review.".to_string(),
- },
- _ => PhaseDeliverables {
- phase: phase.to_string(),
- deliverables: vec![],
- requires_repository: false,
- requires_tasks: false,
- guidance: "Unknown phase for specification contract type".to_string(),
- },
- }
-}
-
-/// Get deliverables for 'execute' contract type
-fn get_execute_type_deliverables(phase: &str) -> PhaseDeliverables {
- match phase {
- "execute" => PhaseDeliverables {
- phase: "execute".to_string(),
- deliverables: vec![], // No deliverables for execute-only contract type
- requires_repository: true,
- requires_tasks: true,
- guidance: "Execute the tasks directly. No deliverable documents are required for this contract type.".to_string(),
- },
- _ => PhaseDeliverables {
- phase: phase.to_string(),
- deliverables: vec![],
- requires_repository: false,
- requires_tasks: false,
- guidance: "The 'execute' contract type only supports the 'execute' phase.".to_string(),
- },
- }
-}
-
-/// Build a phase checklist comparing expected vs actual deliverables
-pub fn get_phase_checklist_for_type(
- phase: &str,
- completed_deliverables: &[String],
- tasks: &[TaskInfo],
- has_repository: bool,
- contract_type: &str,
-) -> PhaseChecklist {
- let phase_config = get_phase_deliverables_for_type(phase, contract_type);
-
- // Build deliverable status list
- let deliverables: Vec<DeliverableStatus> = phase_config
- .deliverables
- .iter()
- .map(|d| DeliverableStatus {
- id: d.id.clone(),
- name: d.name.clone(),
- priority: d.priority,
- completed: completed_deliverables.contains(&d.id),
- })
- .collect();
-
- // Calculate task stats for execute phase
- let task_stats = if phase == "execute" {
- let total = tasks.len();
- let pending = tasks.iter().filter(|t| t.status == "pending").count();
- let running = tasks.iter().filter(|t| t.status == "running").count();
- let done = tasks.iter().filter(|t| t.status == "done").count();
- let failed = tasks
- .iter()
- .filter(|t| t.status == "failed" || t.status == "error")
- .count();
-
- Some(TaskStats {
- total,
- pending,
- running,
- done,
- failed,
- })
- } else {
- None
- };
-
- // Calculate completion percentage
- let mut completed_items = 0;
- let mut total_items = 0;
-
- // Count required and recommended deliverables (not optional)
- for status in &deliverables {
- if status.priority != DeliverablePriority::Optional {
- total_items += 1;
- if status.completed {
- completed_items += 1;
- }
- }
- }
-
- // Count repository if required
- if phase_config.requires_repository {
- total_items += 1;
- if has_repository {
- completed_items += 1;
- }
- }
-
- // Count tasks if in execute phase
- if let Some(ref stats) = task_stats {
- if stats.total > 0 {
- total_items += 1;
- if stats.done == stats.total && stats.total > 0 {
- completed_items += 1;
- }
- }
- }
-
- let completion_percentage = if total_items > 0 {
- ((completed_items as f64 / total_items as f64) * 100.0) as u8
- } else {
- 100 // No requirements means complete
- };
-
- // Generate suggestions
- let mut suggestions = Vec::new();
-
- // Suggest missing deliverables
- for status in &deliverables {
- if !status.completed {
- match status.priority {
- DeliverablePriority::Required => {
- suggestions.push(format!(
- "Mark '{}' as complete using mark_deliverable_complete (required)",
- status.name
- ));
- }
- DeliverablePriority::Recommended => {
- suggestions.push(format!(
- "Consider completing '{}' (recommended)",
- status.name
- ));
- }
- DeliverablePriority::Optional => {}
- }
- }
- }
-
- // Suggest repository if needed
- if phase_config.requires_repository && !has_repository {
- suggestions.push("Configure a repository for task execution".to_string());
- }
-
- // Suggest task actions for execute phase
- if let Some(ref stats) = task_stats {
- if stats.total == 0 {
- suggestions.push("Create tasks to implement the plan".to_string());
- } else if stats.pending > 0 {
- suggestions.push(format!("Run {} pending task(s)", stats.pending));
- } else if stats.running > 0 {
- suggestions.push(format!(
- "Wait for {} running task(s) to complete",
- stats.running
- ));
- } else if stats.failed > 0 {
- suggestions.push(format!("Address {} failed task(s)", stats.failed));
- } else if stats.done == stats.total && stats.total > 0 {
- suggestions.push("All tasks complete. Mark deliverables and advance phase.".to_string());
- }
- }
-
- // Generate summary
- let summary = generate_phase_summary(
- phase,
- &deliverables,
- has_repository,
- &task_stats,
- completion_percentage,
- );
-
- PhaseChecklist {
- phase: phase.to_string(),
- deliverables,
- has_repository,
- repository_required: phase_config.requires_repository,
- task_stats,
- completion_percentage,
- summary,
- suggestions,
- }
-}
-
-fn generate_phase_summary(
- phase: &str,
- deliverables: &[DeliverableStatus],
- has_repository: bool,
- task_stats: &Option<TaskStats>,
- completion_percentage: u8,
-) -> String {
- let completed_count = deliverables.iter().filter(|d| d.completed).count();
- let total_count = deliverables.len();
-
- match phase {
- "research" => {
- if completed_count == 0 {
- "Research phase needs documentation. Mark deliverables complete when ready."
- .to_string()
- } else {
- format!(
- "{}/{} deliverables complete. Ready to transition to Specify phase.",
- completed_count, total_count
- )
- }
- }
- "specify" => {
- let has_required = deliverables
- .iter()
- .filter(|d| d.priority == DeliverablePriority::Required)
- .all(|d| d.completed);
-
- if !has_required {
- "Specify phase requires completing the Requirements Document deliverable."
- .to_string()
- } else {
- "Specifications ready. Consider transitioning to Plan phase.".to_string()
- }
- }
- "plan" => {
- let has_required = deliverables
- .iter()
- .filter(|d| d.priority == DeliverablePriority::Required)
- .all(|d| d.completed);
-
- if !has_required {
- "Plan phase requires completing the Plan deliverable.".to_string()
- } else if !has_repository {
- "Repository not configured. Configure a repository before Execute phase."
- .to_string()
- } else {
- "Planning complete. Ready to transition to Execute phase.".to_string()
- }
- }
- "execute" => {
- if let Some(stats) = task_stats {
- if stats.total == 0 {
- "No tasks created. Create tasks to implement the plan.".to_string()
- } else if stats.done == stats.total {
- "All tasks complete! Mark deliverables and advance to Review phase (or complete contract).".to_string()
- } else {
- format!(
- "{}/{} tasks completed ({}% done)",
- stats.done,
- stats.total,
- if stats.total > 0 {
- (stats.done * 100) / stats.total
- } else {
- 0
- }
- )
- }
- } else {
- "Execute phase in progress.".to_string()
- }
- }
- "review" => {
- let has_required = deliverables
- .iter()
- .filter(|d| d.priority == DeliverablePriority::Required)
- .all(|d| d.completed);
-
- if !has_required {
- "Review phase requires completing the Release Notes deliverable.".to_string()
- } else {
- "Review documentation complete. Contract can be marked as done.".to_string()
- }
- }
- _ => format!("Phase {} - {}% complete", phase, completion_percentage),
- }
-}
-
-/// Result of checking if deliverables are met for the current phase
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
-pub struct DeliverableCheckResult {
- /// Whether all required deliverables are met
- pub deliverables_met: bool,
- /// Whether the phase is ready to advance (includes all readiness checks)
- pub ready_to_advance: bool,
- /// Current phase
- pub phase: String,
- /// Next phase (if available)
- pub next_phase: Option<String>,
- /// List of required deliverables and their status
- pub required_deliverables: Vec<DeliverableItem>,
- /// List of what's missing (if any)
- pub missing: Vec<String>,
- /// Human-readable summary
- pub summary: String,
- /// Whether auto-progress is recommended
- pub auto_progress_recommended: bool,
-}
-
-/// A single deliverable item status
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
-pub struct DeliverableItem {
- /// ID of the deliverable
- pub id: String,
- /// Name of the deliverable
- pub name: String,
- /// Type: "deliverable", "repository", "tasks"
- pub deliverable_type: String,
- /// Whether it's met
- pub met: bool,
- /// Additional details
- pub details: Option<String>,
-}
-
-/// Check if all required deliverables for the current phase are met
-pub fn check_deliverables_met(
- phase: &str,
- contract_type: &str,
- completed_deliverables: &[String],
- tasks: &[TaskInfo],
- has_repository: bool,
-) -> DeliverableCheckResult {
- let mut required_items = Vec::new();
- let mut missing = Vec::new();
-
- // Get the deliverables for this contract type and phase
- let phase_config = get_phase_deliverables_for_type(phase, contract_type);
-
- // Check required deliverables for this phase
- for deliverable in &phase_config.deliverables {
- if deliverable.priority == DeliverablePriority::Required {
- let is_complete = completed_deliverables.contains(&deliverable.id);
-
- required_items.push(DeliverableItem {
- id: deliverable.id.clone(),
- name: deliverable.name.clone(),
- deliverable_type: "deliverable".to_string(),
- met: is_complete,
- details: if is_complete {
- Some("Marked complete".to_string())
- } else {
- None
- },
- });
-
- if !is_complete {
- missing.push(format!(
- "Mark '{}' as complete (required)",
- deliverable.name
- ));
- }
- }
- }
-
- // Check repository for phases that require it
- if phase_config.requires_repository {
- required_items.push(DeliverableItem {
- id: "repository".to_string(),
- name: "Repository".to_string(),
- deliverable_type: "repository".to_string(),
- met: has_repository,
- details: if has_repository {
- Some("Repository configured".to_string())
- } else {
- None
- },
- });
-
- if !has_repository {
- missing.push("Configure a repository".to_string());
- }
- }
-
- // Check tasks for execute phase
- if phase_config.requires_tasks {
- let total_tasks = tasks.len();
- let done_tasks = tasks.iter().filter(|t| t.status == "done").count();
- let tasks_complete = total_tasks > 0 && done_tasks == total_tasks;
-
- required_items.push(DeliverableItem {
- id: "tasks".to_string(),
- name: "Tasks Completed".to_string(),
- deliverable_type: "tasks".to_string(),
- met: tasks_complete,
- details: Some(format!("{}/{} tasks done", done_tasks, total_tasks)),
- });
-
- if !tasks_complete {
- if total_tasks == 0 {
- missing.push("Create and complete tasks".to_string());
- } else {
- missing.push(format!(
- "Complete remaining {} task(s)",
- total_tasks - done_tasks
- ));
- }
- }
- }
-
- let deliverables_met = required_items.iter().all(|d| d.met);
- let next_phase = get_next_phase_for_contract(contract_type, phase);
- let ready_to_advance = deliverables_met && next_phase.is_some();
-
- let summary = if deliverables_met {
- if let Some(ref next) = next_phase {
- format!(
- "All deliverables met for {} phase. Ready to advance to {} phase.",
- phase, next
- )
- } else {
- format!(
- "All deliverables met for {} phase. This is the final phase.",
- phase
- )
- }
- } else {
- format!(
- "{} deliverable(s) still needed for {} phase.",
- missing.len(),
- phase
- )
- };
-
- DeliverableCheckResult {
- deliverables_met,
- ready_to_advance,
- phase: phase.to_string(),
- next_phase,
- required_deliverables: required_items,
- missing,
- summary,
- auto_progress_recommended: deliverables_met && ready_to_advance,
- }
-}
-
-/// Get the next phase based on contract type
-pub fn get_next_phase_for_contract(contract_type: &str, current_phase: &str) -> Option<String> {
- match contract_type {
- "simple" => match current_phase {
- "plan" => Some("execute".to_string()),
- "execute" => None, // Terminal phase for simple contracts
- _ => None,
- },
- "execute" => None, // Execute-only contracts don't have phase transitions
- "specification" | _ => match current_phase {
- "research" => Some("specify".to_string()),
- "specify" => Some("plan".to_string()),
- "plan" => Some("execute".to_string()),
- "execute" => Some("review".to_string()),
- "review" => None, // Final phase
- _ => None,
- },
- }
-}
-
-/// Determine if the contract should auto-progress to the next phase
-pub fn should_auto_progress(
- phase: &str,
- contract_type: &str,
- completed_deliverables: &[String],
- tasks: &[TaskInfo],
- has_repository: bool,
- autonomous_loop: bool,
-) -> AutoProgressDecision {
- let check = check_deliverables_met(
- phase,
- contract_type,
- completed_deliverables,
- tasks,
- has_repository,
- );
-
- if !check.deliverables_met {
- return AutoProgressDecision {
- should_progress: false,
- next_phase: None,
- reason: format!("Deliverables not met: {}", check.missing.join(", ")),
- action: AutoProgressAction::WaitForDeliverables,
- };
- }
-
- if check.next_phase.is_none() {
- return AutoProgressDecision {
- should_progress: false,
- next_phase: None,
- reason: "This is the terminal phase. Contract can be completed.".to_string(),
- action: AutoProgressAction::CompleteContract,
- };
- }
-
- if autonomous_loop {
- AutoProgressDecision {
- should_progress: true,
- next_phase: check.next_phase,
- reason: "All deliverables met and autonomous_loop is enabled.".to_string(),
- action: AutoProgressAction::AdvancePhase,
- }
- } else {
- AutoProgressDecision {
- should_progress: false,
- next_phase: check.next_phase,
- reason: "All deliverables met. Suggest advancing to next phase.".to_string(),
- action: AutoProgressAction::SuggestAdvance,
- }
- }
-}
-
-/// Result of auto-progress decision
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct AutoProgressDecision {
- /// Whether to automatically progress
- pub should_progress: bool,
- /// The next phase to progress to
- pub next_phase: Option<String>,
- /// Reason for the decision
- pub reason: String,
- /// Recommended action
- pub action: AutoProgressAction,
-}
-
-/// Actions that can be taken based on auto-progress decision
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub enum AutoProgressAction {
- /// Wait for required deliverables
- WaitForDeliverables,
- /// Automatically advance to next phase
- AdvancePhase,
- /// Suggest user to advance (when not autonomous)
- SuggestAdvance,
- /// Contract is complete, mark as done
- CompleteContract,
-}
-
-/// Generate enhanced prompt guidance for deliverable checking
-pub fn generate_deliverable_prompt_guidance(
- phase: &str,
- contract_type: &str,
- check_result: &DeliverableCheckResult,
-) -> String {
- let mut guidance = String::new();
-
- guidance.push_str("\n## Phase Deliverables Status\n\n");
- guidance.push_str(&format!(
- "**Current Phase**: {} | **Contract Type**: {}\n\n",
- capitalize(phase),
- contract_type
- ));
-
- // Show required deliverables checklist
- guidance.push_str("### Required Deliverables Checklist\n");
- for item in &check_result.required_deliverables {
- let status = if item.met { "[x]" } else { "[ ]" };
- let details = item
- .details
- .as_ref()
- .map(|d| format!(" - {}", d))
- .unwrap_or_default();
- guidance.push_str(&format!(
- "{} **{}** ({}){}\n",
- status, item.name, item.deliverable_type, details
- ));
- }
-
- // Show status and next actions
- guidance.push_str("\n### Status\n");
- if check_result.deliverables_met {
- guidance.push_str("**All deliverables are met.**\n\n");
- if let Some(ref next) = check_result.next_phase {
- guidance.push_str(&format!("Ready to advance to **{}** phase.\n", next));
- if check_result.auto_progress_recommended {
- guidance.push_str(&format!("\n**ACTION REQUIRED**: Since all deliverables are met, you should call `advance_phase` with `new_phase=\"{}\"` to progress the contract.\n", next));
- }
- } else {
- guidance.push_str(
- "This is the terminal phase. The contract can be marked as completed.\n",
- );
- }
- } else {
- guidance.push_str("**Deliverables not yet met.**\n\n");
- guidance.push_str("Missing:\n");
- for item in &check_result.missing {
- guidance.push_str(&format!("- {}\n", item));
- }
- guidance.push_str(
- "\nUse `mark_deliverable_complete` to mark deliverables as complete when ready.\n",
- );
- }
-
- guidance
-}
-
-/// Format checklist as markdown for LLM context
-pub fn format_checklist_markdown(checklist: &PhaseChecklist) -> String {
- let mut md = format!(
- "## Phase Progress ({} Phase)\n\n",
- capitalize(&checklist.phase)
- );
-
- // Deliverables
- md.push_str("### Deliverables\n");
- for status in &checklist.deliverables {
- let check = if status.completed { "+" } else { "-" };
- let priority_label = match status.priority {
- DeliverablePriority::Required => " (required)",
- DeliverablePriority::Recommended => " (recommended)",
- DeliverablePriority::Optional => " (optional)",
- };
-
- if status.completed {
- md.push_str(&format!("[{}] {} - completed\n", check, status.name));
- } else {
- md.push_str(&format!("[{}] {}{}\n", check, status.name, priority_label));
- }
- }
-
- // Repository status
- if checklist.repository_required {
- let check = if checklist.has_repository { "+" } else { "-" };
- md.push_str(&format!("[{}] Repository configured (required)\n", check));
- }
-
- // Task stats for execute phase
- if let Some(ref stats) = checklist.task_stats {
- md.push_str("\n### Task Progress\n");
- md.push_str(&format!("- Total: {}\n", stats.total));
- md.push_str(&format!("- Done: {}\n", stats.done));
- if stats.pending > 0 {
- md.push_str(&format!("- Pending: {}\n", stats.pending));
- }
- if stats.running > 0 {
- md.push_str(&format!("- Running: {}\n", stats.running));
- }
- if stats.failed > 0 {
- md.push_str(&format!("- Failed: {}\n", stats.failed));
- }
- }
-
- // Summary
- md.push_str(&format!(
- "\n**Status**: {} ({}% complete)\n",
- checklist.summary, checklist.completion_percentage
- ));
-
- // Suggestions
- if !checklist.suggestions.is_empty() {
- md.push_str("\n**Next Steps**:\n");
- for suggestion in &checklist.suggestions {
- md.push_str(&format!("- {}\n", suggestion));
- }
- }
-
- md
-}
-
-fn capitalize(s: &str) -> String {
- let mut chars = s.chars();
- match chars.next() {
- None => String::new(),
- Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_get_phase_deliverables_simple() {
- let plan = get_phase_deliverables_for_type("plan", "simple");
- assert_eq!(plan.phase, "plan");
- assert!(plan.requires_repository);
- assert_eq!(plan.deliverables.len(), 1);
- assert_eq!(plan.deliverables[0].id, "plan-document");
- assert_eq!(plan.deliverables[0].priority, DeliverablePriority::Required);
-
- let execute = get_phase_deliverables_for_type("execute", "simple");
- assert_eq!(execute.phase, "execute");
- assert!(execute.requires_repository);
- assert!(execute.requires_tasks);
- assert_eq!(execute.deliverables.len(), 1);
- assert_eq!(execute.deliverables[0].id, "pull-request");
- }
-
- #[test]
- fn test_get_phase_deliverables_specification() {
- let research = get_phase_deliverables_for_type("research", "specification");
- assert_eq!(research.deliverables.len(), 1);
- assert_eq!(research.deliverables[0].id, "research-notes");
-
- let specify = get_phase_deliverables_for_type("specify", "specification");
- assert_eq!(specify.deliverables.len(), 1);
- assert_eq!(specify.deliverables[0].id, "requirements-document");
-
- let review = get_phase_deliverables_for_type("review", "specification");
- assert_eq!(review.deliverables.len(), 1);
- assert_eq!(review.deliverables[0].id, "release-notes");
- }
-
- #[test]
- fn test_get_phase_deliverables_execute_type() {
- let execute = get_phase_deliverables_for_type("execute", "execute");
- assert!(execute.deliverables.is_empty());
- assert!(execute.requires_repository);
- assert!(execute.requires_tasks);
- }
-
- #[test]
- fn test_check_deliverables_met() {
- // No deliverables marked complete
- let result = check_deliverables_met("plan", "simple", &[], &[], true);
- assert!(!result.deliverables_met);
- assert!(!result.missing.is_empty());
-
- // Deliverable marked complete
- let completed = vec!["plan-document".to_string()];
- let result = check_deliverables_met("plan", "simple", &completed, &[], true);
- assert!(result.deliverables_met);
- assert!(result.ready_to_advance);
- }
-
- #[test]
- fn test_phase_checklist() {
- let completed = vec!["plan-document".to_string()];
- let checklist = get_phase_checklist_for_type("plan", &completed, &[], true, "simple");
- assert_eq!(checklist.completion_percentage, 100);
- assert!(checklist.deliverables[0].completed);
- }
-}
diff --git a/makima/src/llm/task_output.rs b/makima/src/llm/task_output.rs
deleted file mode 100644
index c7f6990..0000000
--- a/makima/src/llm/task_output.rs
+++ /dev/null
@@ -1,485 +0,0 @@
-//! Task output processing and task derivation utilities.
-//!
-//! This module provides utilities for:
-//! - Parsing task lists from markdown documents
-//! - Analyzing completed task outputs
-//! - Suggesting follow-up actions based on task results
-
-use regex::Regex;
-use serde::{Deserialize, Serialize};
-use uuid::Uuid;
-
-/// A parsed task from a markdown document
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct ParsedTask {
- /// Task name/title
- pub name: String,
- /// Task description or plan
- pub description: Option<String>,
- /// Group/phase this task belongs to
- pub group: Option<String>,
- /// Order within the group (0-indexed)
- pub order: usize,
- /// Whether this task was marked as completed in source
- pub completed: bool,
- /// Dependencies (names of other tasks)
- pub dependencies: Vec<String>,
-}
-
-/// Result of parsing tasks from a document
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct TaskParseResult {
- /// Successfully parsed tasks
- pub tasks: Vec<ParsedTask>,
- /// Groups/phases found
- pub groups: Vec<String>,
- /// Total tasks found
- pub total: usize,
- /// Any parsing warnings
- pub warnings: Vec<String>,
-}
-
-/// Impact on contract phase
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct PhaseImpact {
- /// Current phase
- pub phase: String,
- /// Whether phase targets are now met
- pub targets_met: bool,
- /// Tasks remaining in phase
- pub tasks_remaining: usize,
- /// Suggestion for phase transition
- pub transition_suggestion: Option<String>,
-}
-
-/// Suggested action based on task output
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(tag = "type", rename_all = "snake_case")]
-pub enum SuggestedAction {
- /// Create a follow-up task
- CreateTask {
- name: String,
- plan: String,
- chain_from: Option<Uuid>,
- },
- /// Create a new file from template
- CreateFile {
- template_id: String,
- name: String,
- seed_content: Option<String>,
- },
- /// Update an existing file
- UpdateFile {
- file_id: Uuid,
- file_name: String,
- additions: String,
- },
- /// Advance to next phase
- AdvancePhase {
- to_phase: String,
- },
- /// Run the next chained task
- RunNextTask {
- task_id: Uuid,
- task_name: String,
- },
- /// Mark the contract as completed
- MarkContractComplete {
- contract_id: Uuid,
- },
-}
-
-/// Analysis of a completed task's output
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct TaskOutputAnalysis {
- /// Summary of what was accomplished
- pub summary: String,
- /// Files that were created/modified (from diff)
- pub files_affected: Vec<String>,
- /// Suggested next actions
- pub next_steps: Vec<SuggestedAction>,
- /// Impact on contract phase
- pub phase_impact: Option<PhaseImpact>,
-}
-
-/// Parse tasks from a markdown task breakdown document
-///
-/// Supports formats like:
-/// - `[ ] Task name`
-/// - `[x] Completed task`
-/// - `1. Task name`
-/// - `- Task name`
-///
-/// Groups are detected from `## Phase/Section` headings.
-pub fn parse_tasks_from_breakdown(content: &str) -> TaskParseResult {
- let mut tasks = Vec::new();
- let mut groups = Vec::new();
- let mut warnings = Vec::new();
- let mut current_group: Option<String> = None;
- let mut task_order = 0;
-
- // Patterns for task items
- let checkbox_pattern = Regex::new(r"^\s*[-*]\s*\[([ xX])\]\s*(.+)$").unwrap();
- let numbered_checkbox = Regex::new(r"^\s*\d+\.\s*\[([ xX])\]\s*(.+)$").unwrap();
- let numbered_pattern = Regex::new(r"^\s*\d+\.\s+(.+)$").unwrap();
- let bullet_pattern = Regex::new(r"^\s*[-*]\s+(.+)$").unwrap();
- let heading_pattern = Regex::new(r"^##\s+(?:Phase\s*\d*:?\s*)?(.+)$").unwrap();
-
- // Patterns for dependencies (inline)
- let depends_pattern = Regex::new(r"(?i)\(?\s*(?:depends on|after|requires):?\s*([^)]+)\)?").unwrap();
-
- for line in content.lines() {
- let trimmed = line.trim();
-
- // Skip empty lines
- if trimmed.is_empty() {
- continue;
- }
-
- // Check for section headings
- if let Some(caps) = heading_pattern.captures(trimmed) {
- let group_name = caps[1].trim().to_string();
- if !groups.contains(&group_name) {
- groups.push(group_name.clone());
- }
- current_group = Some(group_name);
- task_order = 0;
- continue;
- }
-
- // Try to parse as a task
- let mut task_name: Option<String> = None;
- let mut completed = false;
-
- // Try checkbox patterns first (more specific)
- if let Some(caps) = checkbox_pattern.captures(trimmed) {
- completed = &caps[1] != " ";
- task_name = Some(caps[2].trim().to_string());
- } else if let Some(caps) = numbered_checkbox.captures(trimmed) {
- completed = &caps[1] != " ";
- task_name = Some(caps[2].trim().to_string());
- } else if let Some(caps) = numbered_pattern.captures(trimmed) {
- task_name = Some(caps[1].trim().to_string());
- } else if let Some(caps) = bullet_pattern.captures(trimmed) {
- // Only treat as task if it looks like a task (has actionable verbs)
- let text = caps[1].trim();
- if looks_like_task(text) {
- task_name = Some(text.to_string());
- }
- }
-
- if let Some(name) = task_name {
- // Skip items that are clearly not tasks
- if name.to_lowercase().starts_with("note:") ||
- name.to_lowercase().starts_with("todo:") && name.len() < 10 ||
- name.starts_with('#') {
- continue;
- }
-
- // Extract dependencies if present
- let dependencies = if let Some(dep_caps) = depends_pattern.captures(&name) {
- dep_caps[1]
- .split(',')
- .map(|s| s.trim().to_string())
- .filter(|s| !s.is_empty())
- .collect()
- } else {
- Vec::new()
- };
-
- // Clean task name (remove dependency info)
- let clean_name = depends_pattern.replace(&name, "").trim().to_string();
-
- // Extract description if there's a colon
- let (final_name, description) = if let Some(idx) = clean_name.find(':') {
- let (n, d) = clean_name.split_at(idx);
- (n.trim().to_string(), Some(d[1..].trim().to_string()))
- } else {
- (clean_name, None)
- };
-
- tasks.push(ParsedTask {
- name: final_name,
- description,
- group: current_group.clone(),
- order: task_order,
- completed,
- dependencies,
- });
-
- task_order += 1;
- }
- }
-
- let total = tasks.len();
-
- // Add warnings
- if tasks.is_empty() {
- warnings.push("No tasks found in document. Ensure tasks are formatted as checkbox items (- [ ] Task) or numbered lists (1. Task).".to_string());
- }
-
- TaskParseResult {
- tasks,
- groups,
- total,
- warnings,
- }
-}
-
-/// Check if text looks like a task (has action verbs at word boundaries)
-fn looks_like_task(text: &str) -> bool {
- let lower = text.to_lowercase();
- let action_verbs = [
- "add", "create", "implement", "build", "write", "fix", "update",
- "refactor", "test", "configure", "set up", "setup", "deploy",
- "integrate", "migrate", "design", "review", "document", "remove",
- "delete", "modify", "change", "improve", "optimize", "enable",
- "disable", "install", "initialize", "define", "extend", "extract",
- ];
-
- // Check if text starts with an action verb (followed by space or end)
- for verb in &action_verbs {
- if lower.starts_with(verb) {
- // Check for word boundary after verb
- let after = &lower[verb.len()..];
- if after.is_empty() || after.starts_with(' ') || after.starts_with('_') {
- return true;
- }
- }
- // Check if verb appears after space with word boundary
- let pattern = format!(" {} ", verb);
- let pattern_end = format!(" {}", verb);
- if lower.contains(&pattern) {
- return true;
- }
- // Check if verb is at the end of string after a space
- if lower.ends_with(&pattern_end) && lower.len() > pattern_end.len() {
- return true;
- }
- }
- false
-}
-
-/// Analyze a completed task's output to suggest next actions
-pub fn analyze_task_output(
- _task_id: Uuid,
- task_name: &str,
- task_result: Option<&str>,
- task_diff: Option<&str>,
- contract_phase: &str,
- total_tasks: usize,
- completed_tasks: usize,
- next_task: Option<(Uuid, String)>,
- dev_notes_file: Option<(Uuid, String)>,
-) -> TaskOutputAnalysis {
- let mut next_steps = Vec::new();
- let mut files_affected = Vec::new();
-
- // Parse files from diff if available
- if let Some(diff) = task_diff {
- files_affected = extract_files_from_diff(diff);
- }
-
- // Generate summary
- let summary = if let Some(result) = task_result {
- if result.len() > 200 {
- format!("{}...", &result[..200])
- } else {
- result.to_string()
- }
- } else {
- format!("Task '{}' completed", task_name)
- };
-
- // If there's a next chained task, suggest running it
- if let Some((next_id, next_name)) = next_task {
- next_steps.push(SuggestedAction::RunNextTask {
- task_id: next_id,
- task_name: next_name,
- });
- }
-
- // Suggest updating Dev Notes if in execute phase and file exists
- if contract_phase == "execute" {
- if let Some((file_id, file_name)) = dev_notes_file {
- let additions = format!(
- "\n## Task: {}\n\n{}\n\n### Files Modified\n{}\n",
- task_name,
- summary,
- files_affected.iter()
- .map(|f| format!("- {}", f))
- .collect::<Vec<_>>()
- .join("\n")
- );
-
- next_steps.push(SuggestedAction::UpdateFile {
- file_id,
- file_name,
- additions,
- });
- } else {
- // Suggest creating Dev Notes
- next_steps.push(SuggestedAction::CreateFile {
- template_id: "dev-notes".to_string(),
- name: "Development Notes".to_string(),
- seed_content: Some(format!(
- "# Development Notes\n\n## Task: {}\n\n{}\n",
- task_name, summary
- )),
- });
- }
- }
-
- // Calculate phase impact
- let new_completed = completed_tasks + 1;
- let targets_met = new_completed >= total_tasks && total_tasks > 0;
- let tasks_remaining = total_tasks.saturating_sub(new_completed);
-
- let transition_suggestion = if targets_met && contract_phase == "execute" {
- Some("All tasks complete. Ready to advance to Review phase.".to_string())
- } else {
- None
- };
-
- // If targets are met, suggest phase transition
- if targets_met && contract_phase == "execute" {
- next_steps.push(SuggestedAction::AdvancePhase {
- to_phase: "review".to_string(),
- });
- }
-
- let phase_impact = Some(PhaseImpact {
- phase: contract_phase.to_string(),
- targets_met,
- tasks_remaining,
- transition_suggestion,
- });
-
- TaskOutputAnalysis {
- summary,
- files_affected,
- next_steps,
- phase_impact,
- }
-}
-
-/// Extract file paths from a git diff
-fn extract_files_from_diff(diff: &str) -> Vec<String> {
- let mut files = Vec::new();
- let file_pattern = Regex::new(r"^(?:diff --git a/|[+]{3} b/|[-]{3} a/)(.+)$").unwrap();
-
- for line in diff.lines() {
- if let Some(caps) = file_pattern.captures(line) {
- let path = caps[1].trim().to_string();
- // Skip /dev/null and duplicates
- if path != "/dev/null" && !files.contains(&path) {
- // Clean up path (remove a/ or b/ prefix from git diff)
- let clean_path = path.trim_start_matches("a/").trim_start_matches("b/").to_string();
- if !files.contains(&clean_path) {
- files.push(clean_path);
- }
- }
- }
- }
-
- files
-}
-
-/// Format parsed tasks for display
-pub fn format_parsed_tasks(result: &TaskParseResult) -> String {
- let mut output = String::new();
-
- if result.tasks.is_empty() {
- output.push_str("No tasks found in the document.\n");
- for warning in &result.warnings {
- output.push_str(&format!("Warning: {}\n", warning));
- }
- return output;
- }
-
- output.push_str(&format!("Found {} task(s)", result.total));
- if !result.groups.is_empty() {
- output.push_str(&format!(" in {} group(s)", result.groups.len()));
- }
- output.push_str(":\n\n");
-
- let mut current_group: Option<&str> = None;
- for (i, task) in result.tasks.iter().enumerate() {
- // Print group header if changed
- if task.group.as_deref() != current_group {
- current_group = task.group.as_deref();
- if let Some(group) = current_group {
- output.push_str(&format!("**{}**\n", group));
- }
- }
-
- let status = if task.completed { "[x]" } else { "[ ]" };
- output.push_str(&format!("{}. {} {}", i + 1, status, task.name));
-
- if !task.dependencies.is_empty() {
- output.push_str(&format!(" (depends on: {})", task.dependencies.join(", ")));
- }
-
- output.push('\n');
- }
-
- output
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_parse_checkbox_tasks() {
- let content = r#"
-## Phase 1: Setup
-- [ ] Set up project structure
-- [x] Configure dev environment
-
-## Phase 2: Features
-1. [ ] Implement authentication
-2. [ ] Add user dashboard
-"#;
-
- let result = parse_tasks_from_breakdown(content);
- assert_eq!(result.total, 4);
- assert_eq!(result.groups.len(), 2);
- assert!(!result.tasks[0].completed);
- assert!(result.tasks[1].completed);
- }
-
- #[test]
- fn test_parse_with_dependencies() {
- let content = r#"
-- [ ] Task A
-- [ ] Task B (depends on: Task A)
-"#;
-
- let result = parse_tasks_from_breakdown(content);
- assert_eq!(result.tasks[1].dependencies, vec!["Task A"]);
- }
-
- #[test]
- fn test_extract_files_from_diff() {
- let diff = r#"
-diff --git a/src/main.rs b/src/main.rs
---- a/src/main.rs
-+++ b/src/main.rs
-@@ -1,3 +1,4 @@
-+fn new_function() {}
-"#;
-
- let files = extract_files_from_diff(diff);
- assert!(files.contains(&"src/main.rs".to_string()));
- }
-
- #[test]
- fn test_looks_like_task() {
- assert!(looks_like_task("Add authentication"));
- assert!(looks_like_task("Create user model"));
- assert!(looks_like_task("implement feature X"));
- assert!(!looks_like_task("This is a note"));
- assert!(!looks_like_task("Summary of changes"));
- }
-}
diff --git a/makima/src/llm/templates.rs b/makima/src/llm/templates.rs
deleted file mode 100644
index 48b7515..0000000
--- a/makima/src/llm/templates.rs
+++ /dev/null
@@ -1,80 +0,0 @@
-//! Contract type template definitions.
-//!
-//! Defines the available contract types and their workflow phases.
-
-use serde::{Deserialize, Serialize};
-use utoipa::ToSchema;
-
-// =============================================================================
-// Contract Type Templates (Workflow Definitions)
-// =============================================================================
-
-/// A contract type template defining a workflow
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct ContractTypeTemplate {
- /// Unique identifier (e.g., 'simple', 'specification', 'feature-development')
- pub id: String,
- /// Display name
- pub name: String,
- /// What this contract type is for
- pub description: String,
- /// Ordered list of phases in the workflow
- pub phases: Vec<String>,
- /// Starting phase
- pub default_phase: String,
- /// True for built-in types ('simple', 'specification')
- pub is_builtin: bool,
-}
-
-/// Get all available contract type templates
-pub fn all_contract_types() -> Vec<ContractTypeTemplate> {
- vec![
- simple_contract_type(),
- specification_contract_type(),
- execute_contract_type(),
- ]
-}
-
-/// Simple contract type with basic plan/execute workflow
-fn simple_contract_type() -> ContractTypeTemplate {
- ContractTypeTemplate {
- id: "simple".to_string(),
- name: "Simple".to_string(),
- description: "A basic workflow for straightforward tasks with planning and execution phases."
- .to_string(),
- phases: vec!["plan".to_string(), "execute".to_string()],
- default_phase: "plan".to_string(),
- is_builtin: true,
- }
-}
-
-/// Specification contract type with full research-to-review workflow
-fn specification_contract_type() -> ContractTypeTemplate {
- ContractTypeTemplate {
- id: "specification".to_string(),
- name: "Specification".to_string(),
- description: "A comprehensive workflow for complex projects requiring research, specification, planning, execution, and review.".to_string(),
- phases: vec![
- "research".to_string(),
- "specify".to_string(),
- "plan".to_string(),
- "execute".to_string(),
- "review".to_string(),
- ],
- default_phase: "research".to_string(),
- is_builtin: true,
- }
-}
-
-/// Execute-only contract type for immediate task execution
-fn execute_contract_type() -> ContractTypeTemplate {
- ContractTypeTemplate {
- id: "execute".to_string(),
- name: "Execute".to_string(),
- description: "A minimal workflow with only an execute phase for immediate task execution without planning documents.".to_string(),
- phases: vec!["execute".to_string()],
- default_phase: "execute".to_string(),
- is_builtin: true,
- }
-}
diff --git a/makima/src/llm/tools.rs b/makima/src/llm/tools.rs
deleted file mode 100644
index c192398..0000000
--- a/makima/src/llm/tools.rs
+++ /dev/null
@@ -1,1675 +0,0 @@
-//! Tool definitions for file editing via LLM.
-
-use jaq_interpret::FilterT;
-use serde::{Deserialize, Serialize};
-use serde_json::json;
-
-use crate::db::models::{BodyElement, ChartType, TranscriptEntry};
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct Tool {
- pub name: String,
- pub description: String,
- pub parameters: serde_json::Value,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct ToolCall {
- pub id: String,
- pub name: String,
- pub arguments: serde_json::Value,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
-pub struct ToolResult {
- pub success: bool,
- pub message: String,
-}
-
-/// Available tools for file editing
-pub static AVAILABLE_TOOLS: once_cell::sync::Lazy<Vec<Tool>> =
- once_cell::sync::Lazy::new(|| {
- vec![
- Tool {
- name: "add_heading".to_string(),
- description: "Add a heading element to the file body".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "level": {
- "type": "integer",
- "description": "Heading level (1-6)",
- "minimum": 1,
- "maximum": 6
- },
- "text": {
- "type": "string",
- "description": "The heading text"
- },
- "position": {
- "type": "integer",
- "description": "Optional position to insert at (0-indexed). If not specified, appends to end."
- }
- },
- "required": ["level", "text"]
- }),
- },
- Tool {
- name: "add_paragraph".to_string(),
- description: "Add a paragraph element to the file body".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "text": {
- "type": "string",
- "description": "The paragraph text"
- },
- "position": {
- "type": "integer",
- "description": "Optional position to insert at (0-indexed). If not specified, appends to end."
- }
- },
- "required": ["text"]
- }),
- },
- Tool {
- name: "add_code".to_string(),
- description: "Add a code block element to the file body".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "content": {
- "type": "string",
- "description": "The code content"
- },
- "language": {
- "type": "string",
- "description": "Optional programming language for syntax highlighting (e.g., 'javascript', 'python', 'rust')"
- },
- "position": {
- "type": "integer",
- "description": "Optional position to insert at (0-indexed). If not specified, appends to end."
- }
- },
- "required": ["content"]
- }),
- },
- Tool {
- name: "add_list".to_string(),
- description: "Add a list element (ordered or unordered) to the file body".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "items": {
- "type": "array",
- "items": { "type": "string" },
- "description": "Array of list item strings"
- },
- "ordered": {
- "type": "boolean",
- "description": "If true, creates a numbered list; if false (default), creates a bullet list"
- },
- "position": {
- "type": "integer",
- "description": "Optional position to insert at (0-indexed). If not specified, appends to end."
- }
- },
- "required": ["items"]
- }),
- },
- Tool {
- name: "add_chart".to_string(),
- description: "Add a chart visualization to the file body. Supports line, bar, pie, and area charts.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "chart_type": {
- "type": "string",
- "enum": ["line", "bar", "pie", "area"],
- "description": "Type of chart to create"
- },
- "title": {
- "type": "string",
- "description": "Optional chart title"
- },
- "data": {
- "type": "array",
- "description": "Array of data points. Each point should have a 'name' field and one or more numeric value fields.",
- "items": {
- "type": "object"
- }
- },
- "config": {
- "type": "object",
- "description": "Optional chart configuration (colors, axes, etc.)"
- },
- "position": {
- "type": "integer",
- "description": "Optional position to insert at (0-indexed). If not specified, appends to end."
- }
- },
- "required": ["chart_type", "data"]
- }),
- },
- Tool {
- name: "remove_element".to_string(),
- description: "Remove an element from the file body by index".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "index": {
- "type": "integer",
- "description": "Index of element to remove (0-indexed)"
- }
- },
- "required": ["index"]
- }),
- },
- Tool {
- name: "update_element".to_string(),
- description: "Update an existing element in the file body. IMPORTANT: You must provide ALL required fields. For heading: type, level (1-6), text. For paragraph: type, text. For code: type, content, language (optional). For list: type, items (array of strings), ordered (boolean). For chart: type, chartType (line/bar/pie/area), data (array of objects).".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "index": {
- "type": "integer",
- "description": "Index of element to update (0-indexed)"
- },
- "element_type": {
- "type": "string",
- "enum": ["heading", "paragraph", "code", "list", "chart"],
- "description": "Type of element"
- },
- "text": {
- "type": "string",
- "description": "Text content (required for heading and paragraph)"
- },
- "level": {
- "type": "integer",
- "description": "Heading level 1-6 (required for heading)"
- },
- "content": {
- "type": "string",
- "description": "Code content (required for code)"
- },
- "language": {
- "type": "string",
- "description": "Programming language for syntax highlighting (optional for code)"
- },
- "items": {
- "type": "array",
- "items": { "type": "string" },
- "description": "List items (required for list)"
- },
- "ordered": {
- "type": "boolean",
- "description": "If true, numbered list; if false, bullet list (for list)"
- },
- "chartType": {
- "type": "string",
- "enum": ["line", "bar", "pie", "area"],
- "description": "Chart type (required for chart)"
- },
- "data": {
- "type": "array",
- "description": "Chart data array (required for chart)",
- "items": { "type": "object" }
- },
- "title": {
- "type": "string",
- "description": "Chart title (optional for chart)"
- }
- },
- "required": ["index", "element_type"]
- }),
- },
- Tool {
- name: "reorder_elements".to_string(),
- description: "Move an element from one position to another".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "from_index": {
- "type": "integer",
- "description": "Current index of the element"
- },
- "to_index": {
- "type": "integer",
- "description": "New index for the element"
- }
- },
- "required": ["from_index", "to_index"]
- }),
- },
- Tool {
- name: "set_summary".to_string(),
- description: "Set the file summary text".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "summary": {
- "type": "string",
- "description": "The summary text"
- }
- },
- "required": ["summary"]
- }),
- },
- Tool {
- name: "parse_csv".to_string(),
- description: "Parse CSV data into JSON format suitable for charts".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "csv": {
- "type": "string",
- "description": "CSV data string with header row"
- }
- },
- "required": ["csv"]
- }),
- },
- Tool {
- name: "clear_body".to_string(),
- description: "Clear all elements from the file body".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {}
- }),
- },
- Tool {
- name: "jq".to_string(),
- description: "Transform JSON data using jq expressions. Useful for filtering, mapping, grouping, and aggregating data before creating charts.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "input": {
- "description": "The JSON data to transform (can be an array or object)"
- },
- "filter": {
- "type": "string",
- "description": "The jq filter expression. Examples: '.[] | select(.value > 10)', 'group_by(.category) | map({name: .[0].category, count: length})', '[.[] | {name: .label, value: .amount}]'"
- }
- },
- "required": ["input", "filter"]
- }),
- },
- // Interactive tools
- Tool {
- name: "ask_user".to_string(),
- description: "Ask the user one or more questions. Use this when you need clarification, want to offer choices, or need user input before proceeding. Questions can be single-select (user picks one option) or multi-select (user can pick multiple options). The question text supports markdown formatting. The conversation will pause until the user responds.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "questions": {
- "type": "array",
- "description": "List of questions to ask the user",
- "items": {
- "type": "object",
- "properties": {
- "id": {
- "type": "string",
- "description": "Unique identifier for this question (e.g., 'chart_type', 'color_scheme')"
- },
- "question": {
- "type": "string",
- "description": "The question to ask the user. Supports markdown formatting (bold, code, lists, etc.)"
- },
- "options": {
- "type": "array",
- "items": { "type": "string" },
- "description": "Multiple choice options for the user to select from"
- },
- "allowMultiple": {
- "type": "boolean",
- "description": "If true, user can select multiple options (multi-select). If false or omitted, user selects exactly one option (single-select). Default: false"
- },
- "allowCustom": {
- "type": "boolean",
- "description": "If true, user can provide a custom text answer instead of selecting from options. Default: true"
- }
- },
- "required": ["id", "question", "options"]
- }
- }
- },
- "required": ["questions"]
- }),
- },
- // Content viewing tools
- Tool {
- name: "view_body".to_string(),
- description: "View the complete body structure with full content of all elements. Returns detailed information about each element including type, index, and full text/data.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {},
- "required": []
- }),
- },
- Tool {
- name: "read_element".to_string(),
- description: "Read the full content of a specific body element by its index. Use this to get complete details of a single element.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "index": {
- "type": "integer",
- "description": "Index of the element to read (0-indexed)"
- }
- },
- "required": ["index"]
- }),
- },
- Tool {
- name: "view_transcript".to_string(),
- description: "View the complete transcript of the file. Returns all transcript entries with speaker names, text, and timestamps.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {},
- "required": []
- }),
- },
- // Version history tools
- Tool {
- name: "list_versions".to_string(),
- description: "List all available versions of the current document. Returns version numbers, sources (user/llm/system), timestamps, and change descriptions.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {},
- "required": []
- }),
- },
- Tool {
- name: "read_version".to_string(),
- description: "Read the content of a specific historical version of the document. This is read-only and does not modify the current document.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "version": {
- "type": "integer",
- "description": "The version number to read"
- }
- },
- "required": ["version"]
- }),
- },
- Tool {
- name: "restore_version".to_string(),
- description: "Restore the document to a previous version. This creates a new version with the content from the target version. The current content will be preserved as a historical version.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "target_version": {
- "type": "integer",
- "description": "The version number to restore to"
- },
- "reason": {
- "type": "string",
- "description": "Optional reason for the restore (will be recorded in change description)"
- }
- },
- "required": ["target_version"]
- }),
- },
- ]
- });
-
-/// Request for version-related operations that require async database access
-#[derive(Debug, Clone)]
-pub enum VersionToolRequest {
- /// List all versions of the current file
- ListVersions,
- /// Read a specific version
- ReadVersion { version: i32 },
- /// Restore to a specific version
- RestoreVersion { target_version: i32, reason: Option<String> },
-}
-
-/// A question to ask the user
-#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct UserQuestion {
- /// Unique identifier for this question
- pub id: String,
- /// The question text
- pub question: String,
- /// Multiple choice options
- pub options: Vec<String>,
- /// Whether multiple options can be selected
- #[serde(default)]
- pub allow_multiple: bool,
- /// Whether a custom answer is allowed
- #[serde(default = "default_allow_custom")]
- pub allow_custom: bool,
-}
-
-fn default_allow_custom() -> bool {
- true
-}
-
-/// User's answer to a question
-#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct UserAnswer {
- /// Question ID this answers
- pub id: String,
- /// Selected option(s) or custom answer
- pub answers: Vec<String>,
-}
-
-/// Result of executing a tool call with modified file state
-#[derive(Debug)]
-pub struct ToolExecutionResult {
- pub result: ToolResult,
- pub new_body: Option<Vec<BodyElement>>,
- pub new_summary: Option<String>,
- pub parsed_data: Option<serde_json::Value>,
- /// Request for async version operations (handled by chat handler)
- pub version_request: Option<VersionToolRequest>,
- /// Questions to ask the user (pauses conversation until answered)
- pub pending_questions: Option<Vec<UserQuestion>>,
-}
-
-/// Execute a tool call and return the result along with any state changes
-pub fn execute_tool_call(
- call: &ToolCall,
- current_body: &[BodyElement],
- current_summary: Option<&str>,
- transcript: &[TranscriptEntry],
-) -> ToolExecutionResult {
- match call.name.as_str() {
- "add_heading" => execute_add_heading(call, current_body),
- "add_paragraph" => execute_add_paragraph(call, current_body),
- "add_code" => execute_add_code(call, current_body),
- "add_list" => execute_add_list(call, current_body),
- "add_chart" => execute_add_chart(call, current_body),
- "remove_element" => execute_remove_element(call, current_body),
- "update_element" => execute_update_element(call, current_body),
- "reorder_elements" => execute_reorder_elements(call, current_body),
- "set_summary" => execute_set_summary(call, current_summary),
- "parse_csv" => execute_parse_csv(call),
- "clear_body" => execute_clear_body(),
- "jq" => execute_jq(call),
- // Interactive tools
- "ask_user" => execute_ask_user(call),
- // Content viewing tools
- "view_body" => execute_view_body(current_body),
- "read_element" => execute_read_element(call, current_body),
- "view_transcript" => execute_view_transcript(transcript),
- // Version history tools - return request for async handling
- "list_versions" => execute_list_versions(),
- "read_version" => execute_read_version(call),
- "restore_version" => execute_restore_version(call),
- _ => ToolExecutionResult {
- result: ToolResult {
- success: false,
- message: format!("Unknown tool: {}", call.name),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- },
- }
-}
-
-fn execute_ask_user(call: &ToolCall) -> ToolExecutionResult {
- let questions_value = call.arguments.get("questions");
-
- let Some(questions_array) = questions_value.and_then(|v| v.as_array()) else {
- return ToolExecutionResult {
- result: ToolResult {
- success: false,
- message: "Missing or invalid 'questions' parameter".to_string(),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- };
- };
-
- let mut questions: Vec<UserQuestion> = Vec::new();
-
- for q in questions_array {
- let id = q.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();
- let question = q.get("question").and_then(|v| v.as_str()).unwrap_or("").to_string();
- let options: Vec<String> = q
- .get("options")
- .and_then(|v| v.as_array())
- .map(|arr| {
- arr.iter()
- .filter_map(|o| o.as_str())
- .map(|s| s.to_string())
- .collect()
- })
- .unwrap_or_default();
- let allow_multiple = q.get("allowMultiple").and_then(|v| v.as_bool()).unwrap_or(false);
- let allow_custom = q.get("allowCustom").and_then(|v| v.as_bool()).unwrap_or(true);
-
- if id.is_empty() || question.is_empty() || options.is_empty() {
- continue;
- }
-
- questions.push(UserQuestion {
- id,
- question,
- options,
- allow_multiple,
- allow_custom,
- });
- }
-
- if questions.is_empty() {
- return ToolExecutionResult {
- result: ToolResult {
- success: false,
- message: "No valid questions provided".to_string(),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- };
- }
-
- let question_count = questions.len();
- ToolExecutionResult {
- result: ToolResult {
- success: true,
- message: format!("Asking user {} question(s). Waiting for response...", question_count),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: Some(questions),
- }
-}
-
-fn execute_add_heading(call: &ToolCall, current_body: &[BodyElement]) -> ToolExecutionResult {
- let level = call.arguments.get("level").and_then(|v| v.as_u64()).unwrap_or(1) as u8;
- let text = call
- .arguments
- .get("text")
- .and_then(|v| v.as_str())
- .unwrap_or("")
- .to_string();
- let position = call.arguments.get("position").and_then(|v| v.as_u64());
-
- let element = BodyElement::Heading { level, text: text.clone() };
- let mut new_body = current_body.to_vec();
-
- if let Some(pos) = position {
- let pos = pos as usize;
- if pos <= new_body.len() {
- new_body.insert(pos, element);
- } else {
- new_body.push(element);
- }
- } else {
- new_body.push(element);
- }
-
- ToolExecutionResult {
- result: ToolResult {
- success: true,
- message: format!("Added heading: {}", text),
- },
- new_body: Some(new_body),
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- }
-}
-
-fn execute_add_paragraph(call: &ToolCall, current_body: &[BodyElement]) -> ToolExecutionResult {
- let text = call
- .arguments
- .get("text")
- .and_then(|v| v.as_str())
- .unwrap_or("")
- .to_string();
- let position = call.arguments.get("position").and_then(|v| v.as_u64());
-
- let element = BodyElement::Paragraph { text: text.clone() };
- let mut new_body = current_body.to_vec();
-
- if let Some(pos) = position {
- let pos = pos as usize;
- if pos <= new_body.len() {
- new_body.insert(pos, element);
- } else {
- new_body.push(element);
- }
- } else {
- new_body.push(element);
- }
-
- let preview = if text.len() > 50 {
- format!("{}...", &text[..50])
- } else {
- text
- };
-
- ToolExecutionResult {
- result: ToolResult {
- success: true,
- message: format!("Added paragraph: {}", preview),
- },
- new_body: Some(new_body),
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- }
-}
-
-fn execute_add_code(call: &ToolCall, current_body: &[BodyElement]) -> ToolExecutionResult {
- let language = call
- .arguments
- .get("language")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string());
- let content = call
- .arguments
- .get("content")
- .and_then(|v| v.as_str())
- .unwrap_or("")
- .to_string();
- let position = call.arguments.get("position").and_then(|v| v.as_u64());
-
- let element = BodyElement::Code {
- language: language.clone(),
- content: content.clone(),
- };
- let mut new_body = current_body.to_vec();
-
- if let Some(pos) = position {
- let pos = pos as usize;
- if pos <= new_body.len() {
- new_body.insert(pos, element);
- } else {
- new_body.push(element);
- }
- } else {
- new_body.push(element);
- }
-
- let lang_str = language.as_deref().unwrap_or("plain");
- let preview: String = content.chars().take(50).collect();
-
- ToolExecutionResult {
- result: ToolResult {
- success: true,
- message: format!("Added code block ({}): {}", lang_str, preview),
- },
- new_body: Some(new_body),
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- }
-}
-
-fn execute_add_list(call: &ToolCall, current_body: &[BodyElement]) -> ToolExecutionResult {
- let ordered = call
- .arguments
- .get("ordered")
- .and_then(|v| v.as_bool())
- .unwrap_or(false);
- let items: Vec<String> = call
- .arguments
- .get("items")
- .and_then(|v| v.as_array())
- .map(|arr| {
- arr.iter()
- .filter_map(|v| v.as_str().map(|s| s.to_string()))
- .collect()
- })
- .unwrap_or_default();
- let position = call.arguments.get("position").and_then(|v| v.as_u64());
-
- let element = BodyElement::List {
- ordered,
- items: items.clone(),
- };
- let mut new_body = current_body.to_vec();
-
- if let Some(pos) = position {
- let pos = pos as usize;
- if pos <= new_body.len() {
- new_body.insert(pos, element);
- } else {
- new_body.push(element);
- }
- } else {
- new_body.push(element);
- }
-
- let list_type = if ordered { "ordered" } else { "unordered" };
-
- ToolExecutionResult {
- result: ToolResult {
- success: true,
- message: format!("Added {} list with {} items", list_type, items.len()),
- },
- new_body: Some(new_body),
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- }
-}
-
-fn execute_add_chart(call: &ToolCall, current_body: &[BodyElement]) -> ToolExecutionResult {
- let chart_type_str = call
- .arguments
- .get("chart_type")
- .and_then(|v| v.as_str())
- .unwrap_or("bar");
-
- let chart_type = match chart_type_str {
- "line" => ChartType::Line,
- "bar" => ChartType::Bar,
- "pie" => ChartType::Pie,
- "area" => ChartType::Area,
- _ => ChartType::Bar,
- };
-
- let title = call
- .arguments
- .get("title")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string());
-
- let data = call
- .arguments
- .get("data")
- .cloned()
- .unwrap_or(json!([]));
-
- let config = call.arguments.get("config").cloned();
- let position = call.arguments.get("position").and_then(|v| v.as_u64());
-
- let element = BodyElement::Chart {
- chart_type,
- title: title.clone(),
- data,
- config,
- };
-
- let mut new_body = current_body.to_vec();
-
- if let Some(pos) = position {
- let pos = pos as usize;
- if pos <= new_body.len() {
- new_body.insert(pos, element);
- } else {
- new_body.push(element);
- }
- } else {
- new_body.push(element);
- }
-
- ToolExecutionResult {
- result: ToolResult {
- success: true,
- message: format!(
- "Added {} chart{}",
- chart_type_str,
- title.map(|t| format!(": {}", t)).unwrap_or_default()
- ),
- },
- new_body: Some(new_body),
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- }
-}
-
-fn execute_remove_element(call: &ToolCall, current_body: &[BodyElement]) -> ToolExecutionResult {
- let index = call.arguments.get("index").and_then(|v| v.as_u64());
-
- let Some(index) = index else {
- return ToolExecutionResult {
- result: ToolResult {
- success: false,
- message: "Missing index parameter".to_string(),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- };
- };
-
- let index = index as usize;
- if index >= current_body.len() {
- return ToolExecutionResult {
- result: ToolResult {
- success: false,
- message: format!("Index {} out of bounds (body has {} elements)", index, current_body.len()),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- };
- }
-
- let mut new_body = current_body.to_vec();
- new_body.remove(index);
-
- ToolExecutionResult {
- result: ToolResult {
- success: true,
- message: format!("Removed element at index {}", index),
- },
- new_body: Some(new_body),
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- }
-}
-
-fn execute_update_element(call: &ToolCall, current_body: &[BodyElement]) -> ToolExecutionResult {
- let index = call.arguments.get("index").and_then(|v| v.as_u64());
- let element_type = call.arguments.get("element_type").and_then(|v| v.as_str());
-
- let Some(index) = index else {
- return ToolExecutionResult {
- result: ToolResult {
- success: false,
- message: "Missing index parameter".to_string(),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- };
- };
-
- let Some(element_type) = element_type else {
- return ToolExecutionResult {
- result: ToolResult {
- success: false,
- message: "Missing element_type parameter".to_string(),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- };
- };
-
- let index = index as usize;
- if index >= current_body.len() {
- return ToolExecutionResult {
- result: ToolResult {
- success: false,
- message: format!("Index {} out of bounds (body has {} elements)", index, current_body.len()),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- };
- }
-
- // Build the element based on type
- let new_element = match element_type {
- "heading" => {
- let level = call.arguments.get("level").and_then(|v| v.as_u64()).unwrap_or(1) as u8;
- let text = call.arguments.get("text").and_then(|v| v.as_str()).unwrap_or("").to_string();
- BodyElement::Heading { level, text }
- }
- "paragraph" => {
- let text = call.arguments.get("text").and_then(|v| v.as_str()).unwrap_or("").to_string();
- BodyElement::Paragraph { text }
- }
- "code" => {
- let language = call.arguments.get("language").and_then(|v| v.as_str()).map(|s| s.to_string());
- let content = call.arguments.get("content").and_then(|v| v.as_str()).unwrap_or("").to_string();
- BodyElement::Code { language, content }
- }
- "list" => {
- let ordered = call.arguments.get("ordered").and_then(|v| v.as_bool()).unwrap_or(false);
- let items = call.arguments.get("items")
- .and_then(|v| v.as_array())
- .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
- .unwrap_or_default();
- BodyElement::List { ordered, items }
- }
- "chart" => {
- let chart_type_str = call.arguments.get("chartType").and_then(|v| v.as_str()).unwrap_or("bar");
- let chart_type = match chart_type_str {
- "line" => ChartType::Line,
- "bar" => ChartType::Bar,
- "pie" => ChartType::Pie,
- "area" => ChartType::Area,
- _ => ChartType::Bar,
- };
- let title = call.arguments.get("title").and_then(|v| v.as_str()).map(|s| s.to_string());
- let data = call.arguments.get("data").cloned().unwrap_or(json!([]));
- let config = call.arguments.get("config").cloned();
- BodyElement::Chart { chart_type, title, data, config }
- }
- _ => {
- return ToolExecutionResult {
- result: ToolResult {
- success: false,
- message: format!("Unknown element_type: {}. Must be heading, paragraph, code, list, or chart.", element_type),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- };
- }
- };
-
- let mut new_body = current_body.to_vec();
- new_body[index] = new_element;
-
- ToolExecutionResult {
- result: ToolResult {
- success: true,
- message: format!("Updated element at index {} to {}", index, element_type),
- },
- new_body: Some(new_body),
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- }
-}
-
-fn execute_reorder_elements(call: &ToolCall, current_body: &[BodyElement]) -> ToolExecutionResult {
- let from_index = call.arguments.get("from_index").and_then(|v| v.as_u64());
- let to_index = call.arguments.get("to_index").and_then(|v| v.as_u64());
-
- let (Some(from), Some(to)) = (from_index, to_index) else {
- return ToolExecutionResult {
- result: ToolResult {
- success: false,
- message: "Missing from_index or to_index parameter".to_string(),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- };
- };
-
- let from = from as usize;
- let to = to as usize;
-
- if from >= current_body.len() || to >= current_body.len() {
- return ToolExecutionResult {
- result: ToolResult {
- success: false,
- message: format!(
- "Index out of bounds: from={}, to={}, body has {} elements",
- from, to, current_body.len()
- ),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- };
- }
-
- let mut new_body = current_body.to_vec();
- let element = new_body.remove(from);
- new_body.insert(to, element);
-
- ToolExecutionResult {
- result: ToolResult {
- success: true,
- message: format!("Moved element from index {} to {}", from, to),
- },
- new_body: Some(new_body),
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- }
-}
-
-fn execute_set_summary(call: &ToolCall, _current_summary: Option<&str>) -> ToolExecutionResult {
- let summary = call
- .arguments
- .get("summary")
- .and_then(|v| v.as_str())
- .unwrap_or("")
- .to_string();
-
- ToolExecutionResult {
- result: ToolResult {
- success: true,
- message: "Summary updated".to_string(),
- },
- new_body: None,
- new_summary: Some(summary),
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- }
-}
-
-fn execute_parse_csv(call: &ToolCall) -> ToolExecutionResult {
- let csv = call
- .arguments
- .get("csv")
- .and_then(|v| v.as_str())
- .unwrap_or("");
-
- let lines: Vec<&str> = csv.lines().collect();
- if lines.is_empty() {
- return ToolExecutionResult {
- result: ToolResult {
- success: false,
- message: "Empty CSV data".to_string(),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- };
- }
-
- let headers: Vec<&str> = lines[0].split(',').map(|s| s.trim()).collect();
- let mut data: Vec<serde_json::Value> = Vec::new();
-
- for line in lines.iter().skip(1) {
- if line.trim().is_empty() {
- continue;
- }
- let values: Vec<&str> = line.split(',').map(|s| s.trim()).collect();
- let mut row = serde_json::Map::new();
-
- for (i, header) in headers.iter().enumerate() {
- if let Some(value) = values.get(i) {
- // Try to parse as number, otherwise use string
- if let Ok(num) = value.parse::<f64>() {
- row.insert(header.to_string(), json!(num));
- } else {
- row.insert(header.to_string(), json!(value));
- }
- }
- }
-
- data.push(serde_json::Value::Object(row));
- }
-
- ToolExecutionResult {
- result: ToolResult {
- success: true,
- message: format!("Parsed {} rows from CSV", data.len()),
- },
- new_body: None,
- new_summary: None,
- parsed_data: Some(json!(data)),
- version_request: None,
- pending_questions: None,
- }
-}
-
-fn execute_clear_body() -> ToolExecutionResult {
- ToolExecutionResult {
- result: ToolResult {
- success: true,
- message: "Cleared all body elements".to_string(),
- },
- new_body: Some(vec![]),
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- }
-}
-
-fn execute_jq(call: &ToolCall) -> ToolExecutionResult {
- let input = match call.arguments.get("input") {
- Some(v) => v.clone(),
- None => {
- return ToolExecutionResult {
- result: ToolResult {
- success: false,
- message: "Missing input parameter".to_string(),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- };
- }
- };
-
- let filter = match call.arguments.get("filter").and_then(|v| v.as_str()) {
- Some(f) => f,
- None => {
- return ToolExecutionResult {
- result: ToolResult {
- success: false,
- message: "Missing filter parameter".to_string(),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- };
- }
- };
-
- // Parse the jq filter
- let mut defs = jaq_interpret::ParseCtx::new(Vec::new());
- defs.insert_natives(jaq_core::core());
- defs.insert_defs(jaq_std::std());
-
- let (parsed_filter, errs) = jaq_parse::parse(filter, jaq_parse::main());
- if !errs.is_empty() {
- return ToolExecutionResult {
- result: ToolResult {
- success: false,
- message: format!("Invalid jq filter: {:?}", errs),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- };
- }
-
- let Some(parsed_filter) = parsed_filter else {
- return ToolExecutionResult {
- result: ToolResult {
- success: false,
- message: "Failed to parse jq filter".to_string(),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- };
- };
-
- // Compile the filter
- let compiled = defs.compile(parsed_filter);
- if !defs.errs.is_empty() {
- return ToolExecutionResult {
- result: ToolResult {
- success: false,
- message: format!("Failed to compile jq filter ({} errors)", defs.errs.len()),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- };
- }
-
- // Convert serde_json::Value to jaq Value
- let jaq_input = json_to_jaq(&input);
-
- // Execute the filter
- let inputs = jaq_interpret::RcIter::new(std::iter::empty());
- let mut results: Vec<serde_json::Value> = Vec::new();
-
- for output in compiled.run((jaq_interpret::Ctx::new([], &inputs), jaq_input)) {
- match output {
- Ok(val) => {
- results.push(jaq_to_json(&val));
- }
- Err(e) => {
- return ToolExecutionResult {
- result: ToolResult {
- success: false,
- message: format!("jq execution error: {:?}", e),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- };
- }
- }
- }
-
- // Return single value or array based on results
- let output = if results.len() == 1 {
- results.into_iter().next().unwrap()
- } else {
- json!(results)
- };
-
- let preview = {
- let s = output.to_string();
- if s.len() > 100 {
- format!("{}...", &s[..100])
- } else {
- s
- }
- };
-
- ToolExecutionResult {
- result: ToolResult {
- success: true,
- message: format!("jq transform complete: {}", preview),
- },
- new_body: None,
- new_summary: None,
- parsed_data: Some(output),
- version_request: None,
- pending_questions: None,
- }
-}
-
-// =============================================================================
-// Content Viewing Tool Execution Functions
-// =============================================================================
-
-fn execute_view_body(current_body: &[BodyElement]) -> ToolExecutionResult {
- if current_body.is_empty() {
- return ToolExecutionResult {
- result: ToolResult {
- success: true,
- message: "Body is empty (no elements)".to_string(),
- },
- new_body: None,
- new_summary: None,
- parsed_data: Some(json!([])),
- version_request: None,
- pending_questions: None,
- };
- }
-
- let elements: Vec<serde_json::Value> = current_body
- .iter()
- .enumerate()
- .map(|(i, element)| {
- match element {
- BodyElement::Heading { level, text } => json!({
- "index": i,
- "type": "heading",
- "level": level,
- "text": text
- }),
- BodyElement::Paragraph { text } => json!({
- "index": i,
- "type": "paragraph",
- "text": text
- }),
- BodyElement::Code { language, content } => json!({
- "index": i,
- "type": "code",
- "language": language,
- "content": content
- }),
- BodyElement::List { ordered, items } => json!({
- "index": i,
- "type": "list",
- "ordered": ordered,
- "items": items
- }),
- BodyElement::Chart { chart_type, title, data, config } => json!({
- "index": i,
- "type": "chart",
- "chartType": format!("{:?}", chart_type).to_lowercase(),
- "title": title,
- "data": data,
- "config": config
- }),
- BodyElement::Image { src, alt, caption } => json!({
- "index": i,
- "type": "image",
- "src": src,
- "alt": alt,
- "caption": caption
- }),
- BodyElement::Markdown { content } => json!({
- "index": i,
- "type": "markdown",
- "content": content
- }),
- }
- })
- .collect();
-
- ToolExecutionResult {
- result: ToolResult {
- success: true,
- message: format!("Body contains {} element(s)", current_body.len()),
- },
- new_body: None,
- new_summary: None,
- parsed_data: Some(json!(elements)),
- version_request: None,
- pending_questions: None,
- }
-}
-
-fn execute_read_element(call: &ToolCall, current_body: &[BodyElement]) -> ToolExecutionResult {
- let index = call.arguments.get("index").and_then(|v| v.as_u64());
-
- let Some(index) = index else {
- return ToolExecutionResult {
- result: ToolResult {
- success: false,
- message: "Missing index parameter".to_string(),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- };
- };
-
- let index = index as usize;
- if index >= current_body.len() {
- return ToolExecutionResult {
- result: ToolResult {
- success: false,
- message: format!("Index {} out of bounds (body has {} elements)", index, current_body.len()),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- };
- }
-
- let element = &current_body[index];
- let element_data = match element {
- BodyElement::Heading { level, text } => json!({
- "index": index,
- "type": "heading",
- "level": level,
- "text": text
- }),
- BodyElement::Paragraph { text } => json!({
- "index": index,
- "type": "paragraph",
- "text": text
- }),
- BodyElement::Code { language, content } => json!({
- "index": index,
- "type": "code",
- "language": language,
- "content": content
- }),
- BodyElement::List { ordered, items } => json!({
- "index": index,
- "type": "list",
- "ordered": ordered,
- "items": items
- }),
- BodyElement::Chart { chart_type, title, data, config } => json!({
- "index": index,
- "type": "chart",
- "chartType": format!("{:?}", chart_type).to_lowercase(),
- "title": title,
- "data": data,
- "config": config
- }),
- BodyElement::Image { src, alt, caption } => json!({
- "index": index,
- "type": "image",
- "src": src,
- "alt": alt,
- "caption": caption
- }),
- BodyElement::Markdown { content } => json!({
- "index": index,
- "type": "markdown",
- "content": content
- }),
- };
-
- let type_str = match element {
- BodyElement::Heading { .. } => "heading",
- BodyElement::Paragraph { .. } => "paragraph",
- BodyElement::Code { .. } => "code",
- BodyElement::List { .. } => "list",
- BodyElement::Chart { .. } => "chart",
- BodyElement::Image { .. } => "image",
- BodyElement::Markdown { .. } => "markdown",
- };
-
- ToolExecutionResult {
- result: ToolResult {
- success: true,
- message: format!("Element {} is a {}", index, type_str),
- },
- new_body: None,
- new_summary: None,
- parsed_data: Some(element_data),
- version_request: None,
- pending_questions: None,
- }
-}
-
-fn execute_view_transcript(transcript: &[TranscriptEntry]) -> ToolExecutionResult {
- if transcript.is_empty() {
- return ToolExecutionResult {
- result: ToolResult {
- success: true,
- message: "Transcript is empty".to_string(),
- },
- new_body: None,
- new_summary: None,
- parsed_data: Some(json!([])),
- version_request: None,
- pending_questions: None,
- };
- }
-
- let entries: Vec<serde_json::Value> = transcript
- .iter()
- .enumerate()
- .map(|(i, entry)| {
- json!({
- "index": i,
- "speaker": entry.speaker,
- "text": entry.text,
- "start": entry.start,
- "end": entry.end
- })
- })
- .collect();
-
- // Calculate duration from timestamps
- let duration_info = if let (Some(first), Some(last)) = (transcript.first(), transcript.last()) {
- let duration_secs = last.end - first.start;
- let minutes = (duration_secs / 60.0).floor() as u32;
- let seconds = (duration_secs % 60.0).round() as u32;
- format!(" (duration: {}:{:02})", minutes, seconds)
- } else {
- String::new()
- };
-
- ToolExecutionResult {
- result: ToolResult {
- success: true,
- message: format!("Transcript has {} entries{}", transcript.len(), duration_info),
- },
- new_body: None,
- new_summary: None,
- parsed_data: Some(json!(entries)),
- version_request: None,
- pending_questions: None,
- }
-}
-
-// =============================================================================
-// Version History Tool Execution Functions
-// =============================================================================
-// These return version_request instead of performing the operation directly,
-// because they require async database access which is handled in the chat handler.
-
-fn execute_list_versions() -> ToolExecutionResult {
- ToolExecutionResult {
- result: ToolResult {
- success: true,
- message: "Listing versions...".to_string(),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: Some(VersionToolRequest::ListVersions),
- pending_questions: None,
- }
-}
-
-fn execute_read_version(call: &ToolCall) -> ToolExecutionResult {
- let version = call.arguments.get("version").and_then(|v| v.as_i64());
-
- let Some(version) = version else {
- return ToolExecutionResult {
- result: ToolResult {
- success: false,
- message: "Missing version parameter".to_string(),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- };
- };
-
- ToolExecutionResult {
- result: ToolResult {
- success: true,
- message: format!("Reading version {}...", version),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: Some(VersionToolRequest::ReadVersion { version: version as i32 }),
- pending_questions: None,
- }
-}
-
-fn execute_restore_version(call: &ToolCall) -> ToolExecutionResult {
- let target_version = call.arguments.get("target_version").and_then(|v| v.as_i64());
- let reason = call
- .arguments
- .get("reason")
- .and_then(|v| v.as_str())
- .map(|s| s.to_string());
-
- let Some(target_version) = target_version else {
- return ToolExecutionResult {
- result: ToolResult {
- success: false,
- message: "Missing target_version parameter".to_string(),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: None,
- pending_questions: None,
- };
- };
-
- ToolExecutionResult {
- result: ToolResult {
- success: true,
- message: format!("Restoring to version {}...", target_version),
- },
- new_body: None,
- new_summary: None,
- parsed_data: None,
- version_request: Some(VersionToolRequest::RestoreVersion {
- target_version: target_version as i32,
- reason,
- }),
- pending_questions: None,
- }
-}
-
-/// Convert serde_json::Value to jaq_interpret::Val
-fn json_to_jaq(value: &serde_json::Value) -> jaq_interpret::Val {
- match value {
- serde_json::Value::Null => jaq_interpret::Val::Null,
- serde_json::Value::Bool(b) => jaq_interpret::Val::Bool(*b),
- serde_json::Value::Number(n) => {
- if let Some(i) = n.as_i64() {
- jaq_interpret::Val::Int(i as isize)
- } else if let Some(f) = n.as_f64() {
- jaq_interpret::Val::Float(f)
- } else {
- jaq_interpret::Val::Null
- }
- }
- serde_json::Value::String(s) => jaq_interpret::Val::Str(s.clone().into()),
- serde_json::Value::Array(arr) => {
- jaq_interpret::Val::Arr(std::rc::Rc::new(arr.iter().map(json_to_jaq).collect()))
- }
- serde_json::Value::Object(obj) => {
- let mut map: indexmap::IndexMap<std::rc::Rc<String>, jaq_interpret::Val, ahash::RandomState> =
- indexmap::IndexMap::with_hasher(ahash::RandomState::new());
- for (k, v) in obj {
- map.insert(std::rc::Rc::new(k.clone()), json_to_jaq(v));
- }
- jaq_interpret::Val::Obj(std::rc::Rc::new(map))
- }
- }
-}
-
-/// Convert jaq_interpret::Val to serde_json::Value
-fn jaq_to_json(value: &jaq_interpret::Val) -> serde_json::Value {
- match value {
- jaq_interpret::Val::Null => serde_json::Value::Null,
- jaq_interpret::Val::Bool(b) => json!(*b),
- jaq_interpret::Val::Int(i) => json!(*i),
- jaq_interpret::Val::Float(f) => json!(*f),
- jaq_interpret::Val::Num(n) => {
- // Try to parse the number string
- if let Ok(i) = n.parse::<i64>() {
- json!(i)
- } else if let Ok(f) = n.parse::<f64>() {
- json!(f)
- } else {
- json!(n.as_ref())
- }
- }
- jaq_interpret::Val::Str(s) => json!(s.as_ref()),
- jaq_interpret::Val::Arr(arr) => {
- json!(arr.iter().map(jaq_to_json).collect::<Vec<_>>())
- }
- jaq_interpret::Val::Obj(obj) => {
- let mut map = serde_json::Map::new();
- for (k, v) in obj.iter() {
- map.insert((**k).clone(), jaq_to_json(v));
- }
- serde_json::Value::Object(map)
- }
- }
-}
diff --git a/makima/src/llm/transcript_analyzer.rs b/makima/src/llm/transcript_analyzer.rs
deleted file mode 100644
index 82aa69d..0000000
--- a/makima/src/llm/transcript_analyzer.rs
+++ /dev/null
@@ -1,292 +0,0 @@
-//! Transcript analyzer for extracting requirements, decisions, and action items.
-
-use serde::{Deserialize, Serialize};
-use utoipa::ToSchema;
-use crate::db::models::TranscriptEntry;
-
-/// An extracted requirement from the transcript
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct ExtractedRequirement {
- pub text: String,
- pub speaker: String,
- pub timestamp: f32,
- pub confidence: f32,
- pub category: Option<String>, // functional, technical, non-functional, business
-}
-
-/// An extracted decision from the transcript
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct ExtractedDecision {
- pub text: String,
- pub speaker: String,
- pub timestamp: f32,
- pub confidence: f32,
- pub context: Option<String>,
-}
-
-/// An extracted action item from the transcript
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct ExtractedActionItem {
- pub text: String,
- pub speaker: String,
- pub timestamp: f32,
- pub assignee: Option<String>,
- pub priority: Option<String>,
-}
-
-/// Result of transcript analysis
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct TranscriptAnalysisResult {
- pub requirements: Vec<ExtractedRequirement>,
- pub decisions: Vec<ExtractedDecision>,
- pub action_items: Vec<ExtractedActionItem>,
- pub key_topics: Vec<String>,
- pub suggested_contract_name: Option<String>,
- pub suggested_description: Option<String>,
- pub speaker_summary: Vec<SpeakerStats>,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct SpeakerStats {
- pub speaker: String,
- pub word_count: usize,
- pub speaking_time_seconds: f32,
- pub contribution_percentage: f32,
-}
-
-/// Format transcript entries into readable text for LLM analysis
-pub fn format_transcript_for_analysis(entries: &[TranscriptEntry]) -> String {
- entries
- .iter()
- .map(|e| format!("[{:.1}s] {}: {}", e.start, e.speaker, e.text))
- .collect::<Vec<_>>()
- .join("\n")
-}
-
-/// Calculate speaker statistics from transcript
-pub fn calculate_speaker_stats(entries: &[TranscriptEntry]) -> Vec<SpeakerStats> {
- use std::collections::HashMap;
-
- let mut stats: HashMap<String, (usize, f32)> = HashMap::new();
-
- for entry in entries {
- let word_count = entry.text.split_whitespace().count();
- let duration = entry.end - entry.start;
-
- let (count, time) = stats.entry(entry.speaker.clone()).or_insert((0, 0.0));
- *count += word_count;
- *time += duration;
- }
-
- let total_words: usize = stats.values().map(|(c, _)| c).sum();
- let total_time: f32 = stats.values().map(|(_, t)| t).sum();
-
- // Suppress unused variable warning
- let _ = total_time;
-
- stats
- .into_iter()
- .map(|(speaker, (word_count, speaking_time))| SpeakerStats {
- speaker,
- word_count,
- speaking_time_seconds: speaking_time,
- contribution_percentage: if total_words > 0 {
- (word_count as f32 / total_words as f32) * 100.0
- } else {
- 0.0
- },
- })
- .collect()
-}
-
-/// Build the analysis prompt for the LLM
-pub fn build_analysis_prompt(transcript_text: &str) -> String {
- format!(r#"Analyze this meeting/conversation transcript and extract structured information.
-
-TRANSCRIPT:
-{}
-
-Extract the following information in JSON format:
-
-1. **Requirements**: Statements where someone expresses a need, want, or must-have. Look for phrases like:
- - "we need to...", "it should...", "must have...", "requirement is..."
- - "the system should...", "users need to be able to..."
-
-2. **Decisions**: Explicit decisions made during the conversation. Look for:
- - "let's go with...", "we decided...", "we'll use...", "agreed to..."
- - "the decision is...", "we're going with..."
-
-3. **Action Items**: Tasks or todos mentioned. Look for:
- - "someone needs to...", "we should...", "next step is..."
- - "I'll do...", "can you...", "TODO:..."
-
-4. **Key Topics**: Main subjects discussed
-
-5. **Suggested Contract Name**: A short name (3-5 words) that captures the main goal
-
-6. **Suggested Description**: A 1-2 sentence description of what should be built/done
-
-Return your analysis as JSON with this structure:
-{{
- "requirements": [
- {{"text": "...", "speaker": "Speaker X", "timestamp": 12.5, "confidence": 0.9, "category": "functional"}}
- ],
- "decisions": [
- {{"text": "...", "speaker": "Speaker X", "timestamp": 45.2, "confidence": 0.85, "context": "..."}}
- ],
- "action_items": [
- {{"text": "...", "speaker": "Speaker X", "timestamp": 78.0, "assignee": null, "priority": "high"}}
- ],
- "key_topics": ["topic1", "topic2"],
- "suggested_contract_name": "...",
- "suggested_description": "..."
-}}
-
-Be conservative - only extract items with high confidence. If nothing is found for a category, return an empty array."#, transcript_text)
-}
-
-/// Parse LLM response into analysis result
-pub fn parse_analysis_response(response: &str, speaker_stats: Vec<SpeakerStats>) -> Result<TranscriptAnalysisResult, String> {
- // Try to extract JSON from the response (it might be wrapped in markdown code blocks)
- let json_str = extract_json_from_response(response)?;
-
- #[derive(Deserialize)]
- struct LlmResponse {
- requirements: Option<Vec<ExtractedRequirement>>,
- decisions: Option<Vec<ExtractedDecision>>,
- action_items: Option<Vec<ExtractedActionItem>>,
- key_topics: Option<Vec<String>>,
- suggested_contract_name: Option<String>,
- suggested_description: Option<String>,
- }
-
- let parsed: LlmResponse = serde_json::from_str(&json_str)
- .map_err(|e| format!("Failed to parse LLM response as JSON: {}", e))?;
-
- Ok(TranscriptAnalysisResult {
- requirements: parsed.requirements.unwrap_or_default(),
- decisions: parsed.decisions.unwrap_or_default(),
- action_items: parsed.action_items.unwrap_or_default(),
- key_topics: parsed.key_topics.unwrap_or_default(),
- suggested_contract_name: parsed.suggested_contract_name,
- suggested_description: parsed.suggested_description,
- speaker_summary: speaker_stats,
- })
-}
-
-/// Extract JSON from LLM response (handles markdown code blocks)
-fn extract_json_from_response(response: &str) -> Result<String, String> {
- // Try to find JSON in code blocks first
- if let Some(start) = response.find("```json") {
- if let Some(end) = response[start..].find("```\n").or_else(|| response[start..].rfind("```")) {
- let json_start = start + 7; // Skip "```json"
- let json_end = start + end;
- if json_end > json_start {
- return Ok(response[json_start..json_end].trim().to_string());
- }
- }
- }
-
- // Try plain code blocks
- if let Some(start) = response.find("```") {
- let after_start = start + 3;
- if let Some(end) = response[after_start..].find("```") {
- let json_str = &response[after_start..after_start + end];
- // Skip language identifier if present
- let json_str = if let Some(newline) = json_str.find('\n') {
- &json_str[newline + 1..]
- } else {
- json_str
- };
- return Ok(json_str.trim().to_string());
- }
- }
-
- // Try to find raw JSON (starts with { or [)
- if let Some(start) = response.find('{') {
- if let Some(end) = response.rfind('}') {
- if end > start {
- return Ok(response[start..=end].to_string());
- }
- }
- }
-
- Err("Could not find JSON in LLM response".to_string())
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_format_transcript() {
- let entries = vec![
- TranscriptEntry {
- id: "1".to_string(),
- speaker: "Speaker 0".to_string(),
- start: 0.0,
- end: 2.5,
- text: "Hello world".to_string(),
- is_final: true,
- },
- ];
-
- let formatted = format_transcript_for_analysis(&entries);
- assert!(formatted.contains("[0.0s] Speaker 0: Hello world"));
- }
-
- #[test]
- fn test_speaker_stats() {
- let entries = vec![
- TranscriptEntry {
- id: "1".to_string(),
- speaker: "Speaker 0".to_string(),
- start: 0.0,
- end: 5.0,
- text: "One two three four five".to_string(),
- is_final: true,
- },
- TranscriptEntry {
- id: "2".to_string(),
- speaker: "Speaker 1".to_string(),
- start: 5.0,
- end: 10.0,
- text: "Six seven eight nine ten".to_string(),
- is_final: true,
- },
- ];
-
- let stats = calculate_speaker_stats(&entries);
- assert_eq!(stats.len(), 2);
-
- for s in &stats {
- assert_eq!(s.word_count, 5);
- assert_eq!(s.speaking_time_seconds, 5.0);
- assert!((s.contribution_percentage - 50.0).abs() < 0.1);
- }
- }
-
- #[test]
- fn test_extract_json_from_response() {
- let response = r#"Here is the analysis:
-```json
-{"key": "value"}
-```
-Done."#;
-
- let json = extract_json_from_response(response).unwrap();
- assert_eq!(json, r#"{"key": "value"}"#);
- }
-
- #[test]
- fn test_extract_raw_json() {
- let response = r#"Analysis: {"key": "value"}"#;
- let json = extract_json_from_response(response).unwrap();
- assert_eq!(json, r#"{"key": "value"}"#);
- }
-}