diff options
| author | soryu <soryu@soryu.co> | 2026-01-11 05:52:14 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-15 00:21:16 +0000 |
| commit | 87044a747b47bd83249d61a45842c7f7b2eae56d (patch) | |
| tree | ef2000ce79ffcc2723ef841acef5aa1deb1d5378 /makima/src/llm/contract_tools.rs | |
| parent | 077820c4167c168072d217a1b01df840463a12a8 (diff) | |
| download | soryu-87044a747b47bd83249d61a45842c7f7b2eae56d.tar.gz soryu-87044a747b47bd83249d61a45842c7f7b2eae56d.zip | |
Contract system
Diffstat (limited to 'makima/src/llm/contract_tools.rs')
| -rw-r--r-- | makima/src/llm/contract_tools.rs | 1091 |
1 files changed, 1091 insertions, 0 deletions
diff --git a/makima/src/llm/contract_tools.rs b/makima/src/llm/contract_tools.rs new file mode 100644 index 0000000..0d6f9be --- /dev/null +++ b/makima/src/llm/contract_tools.rs @@ -0,0 +1,1091 @@ +//! 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_file_from_template".to_string(), + description: "Create a new file in the contract from a template. Templates are phase-appropriate document structures.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "template_id": { + "type": "string", + "description": "ID of the template to use (e.g., 'research-notes', 'requirements', 'architecture')" + }, + "name": { + "type": "string", + "description": "Name for the new file" + }, + "description": { + "type": "string", + "description": "Optional description for the file" + } + }, + "required": ["template_id", "name"] + }), + }, + Tool { + name: "create_empty_file".to_string(), + description: "Create a new empty file in the contract without using a template.".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"] + }), + }, + Tool { + name: "list_available_templates".to_string(), + description: "List all available templates, optionally filtered by phase. Use this to see what templates can be used with create_file_from_template.".to_string(), + parameters: json!({ + "type": "object", + "properties": { + "phase": { + "type": "string", + "enum": ["research", "specify", "plan", "execute", "review"], + "description": "Optional filter to show only templates for a specific phase" + } + } + }), + }, + // ============================================================================= + // 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.".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)" + } + }, + "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": {} + }), + }, + // ============================================================================= + // 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.".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" + }, + "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" + }, + "allowCustom": { + "type": "boolean", + "description": "If true, user can provide a custom answer" + } + }, + "required": ["id", "question", "options"] + } + } + }, + "required": ["questions"] + }), + }, + ] +}); + +/// 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 + CreateFileFromTemplate { + template_id: String, + name: String, + description: Option<String>, + }, + CreateEmptyFile { + name: String, + description: Option<String>, + }, + ListAvailableTemplates { 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 }, + + // Repository management + ListDaemonDirectories, + AddRepository { + repo_type: String, + name: String, + url: Option<String>, + is_primary: bool, + }, + SetPrimaryRepository { repository_id: Uuid }, + + // Phase guidance + GetPhaseChecklist, + + // 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>, + }, +} + +/// 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_file_from_template" => parse_create_file_from_template(call), + "create_empty_file" => parse_create_empty_file(call), + "list_available_templates" => parse_list_available_templates(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(), + + // 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), + + _ => 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_file_from_template(call: &super::tools::ToolCall) -> ContractToolExecutionResult { + let template_id = call.arguments.get("template_id").and_then(|v| v.as_str()); + let name = call.arguments.get("name").and_then(|v| v.as_str()); + + let Some(template_id) = template_id else { + return error_result("Missing required parameter: template_id"); + }; + 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 file '{}' from template '{}'...", name, template_id), + data: None, + request: Some(ContractToolRequest::CreateFileFromTemplate { + template_id: template_id.to_string(), + name: name.to_string(), + description, + }), + pending_questions: None, + } +} + +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, + } +} + +fn parse_list_available_templates(call: &super::tools::ToolCall) -> ContractToolExecutionResult { + let phase = call + .arguments + .get("phase") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + ContractToolExecutionResult { + success: true, + message: "Listing available templates...".to_string(), + data: None, + request: Some(ContractToolRequest::ListAvailableTemplates { 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"); + } + + ContractToolExecutionResult { + success: true, + message: format!("Advancing to '{}' phase...", new_phase), + data: None, + request: Some(ContractToolRequest::AdvancePhase { + new_phase: new_phase.to_string(), + }), + 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, + } +} + +// ============================================================================= +// 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, + } +} + +// ============================================================================= +// 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, + } +} |
