use serde::{Deserialize, Serialize}; /// Context in which a tool should be available #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ExecutionContext { Reef, Krill, Both, Agent, } impl Default for ExecutionContext { fn default() -> Self { Self::Both } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolDefinition { #[serde(default)] pub id: String, pub name: String, pub description: String, #[serde(default)] pub category: String, pub parameters: serde_json::Value, #[serde(default)] pub dangerous: bool, #[serde(default)] pub requires_approval: bool, #[serde(default)] pub version: String, /// Whether this tool requires a project context to function. /// Project-scoped tools are filtered out when no project is selected. #[serde(default)] pub project_scoped: bool, /// Maximum execution time in milliseconds. Defaults to 30000 (30s). #[serde(default = "default_timeout_ms")] pub timeout_ms: u64, /// Whether this tool version is deprecated #[serde(default)] pub deprecated: bool, /// Message shown when tool is deprecated, explaining migration path #[serde(default)] pub deprecation_message: String, /// Execution context indicating where this tool should be available #[serde(default)] pub execution_context: ExecutionContext, } const fn default_timeout_ms() -> u64 { 30000 } impl Default for ToolDefinition { fn default() -> Self { Self { id: String::new(), name: String::new(), description: String::new(), category: String::new(), parameters: serde_json::Value::Null, dangerous: false, requires_approval: false, version: String::new(), project_scoped: false, timeout_ms: default_timeout_ms(), deprecated: false, deprecation_message: String::new(), execution_context: ExecutionContext::default(), } } } impl ToolDefinition { pub fn to_openai_format(&self) -> serde_json::Value { serde_json::json!({ "type": "function", "function": { "name": self.name, "description": self.description, "parameters": self.parameters, } }) } pub fn to_short_doc(&self) -> String { format!( "- {}: {}\n", self.name, self.description ) } /// Strip the project_id parameter from the tool's JSON schema. /// This is used to hide the project context from agents that shouldn't /// have to manage it manually. pub fn strip_project_id(&mut self) { if let Some(obj) = self.parameters.as_object_mut() { if let Some(properties) = obj.get_mut("properties").and_then(|p| p.as_object_mut()) { properties.remove("project_id"); } if let Some(required) = obj.get_mut("required").and_then(|r| r.as_array_mut()) { required.retain(|v| v.as_str() != Some("project_id")); } } } /// Check if this tool version is deprecated pub fn is_deprecated(&self) -> bool { self.deprecated } /// Get deprecation message with migration guidance pub fn deprecation_notice(&self) -> Option<&str> { if self.deprecated && !self.deprecation_message.is_empty() { Some(&self.deprecation_message) } else { None } } /// Compare tool versions. Returns true if self is newer than other. /// Uses simple semver comparison (major.minor.patch). pub fn is_newer_than(&self, other: &Self) -> bool { compare_versions(&self.version, &other.version) == std::cmp::Ordering::Greater } /// Filter tools by execution context pub fn filter_by_context(tools: &[ToolDefinition], context: ExecutionContext) -> Vec { match context { ExecutionContext::Both => tools.to_vec(), context => tools .iter() .filter(|t| t.execution_context == context || t.execution_context == ExecutionContext::Both) .cloned() .collect(), } } /// Filter tools to only include non-deprecated ones pub fn filter_active(tools: &[ToolDefinition]) -> Vec { tools .iter() .filter(|t| !t.deprecated) .cloned() .collect() } /// Check compatibility with another tool definition pub fn is_compatible_with(&self, other: &Self) -> bool { self.name == other.name && !self.deprecated && !other.deprecated } } /// Simple semver comparison (major.minor.patch) fn compare_versions(a: &str, b: &str) -> std::cmp::Ordering { let parse_version = |v: &str| -> Vec { v.split('.') .filter_map(|part| part.parse::().ok()) .collect() }; let v1 = parse_version(a); let v2 = parse_version(b); for (p1, p2) in v1.iter().zip(v2.iter()) { match p1.cmp(p2) { std::cmp::Ordering::Equal => continue, non_eq => return non_eq, } } v1.len().cmp(&v2.len()) } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ParsedToolCall { pub tool_name: String, pub call_id: String, pub payload: serde_json::Value, pub raw_json: String, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum ParserState { Text, Collecting, } pub struct ToolParser { state: ParserState, current_tool_name: String, current_accumulator: String, leftover: String, } impl ToolParser { pub fn new() -> Self { Self { state: ParserState::Text, current_tool_name: String::new(), current_accumulator: String::new(), leftover: String::new(), } } pub fn ingest(&mut self, chunk: &str) -> Vec { let mut results = Vec::new(); let text = format!("{}{}", self.leftover, chunk); let mut lines: Vec<&str> = text.split('\n').collect(); // The last element is either empty ( if text ended in \n) or the start of a new line if let Some(last) = lines.pop() { self.leftover = last.to_string(); } else { self.leftover.clear(); } for line in lines { match self.state { ParserState::Text => { let trimmed = line.trim(); if let Some(tool_name) = trimmed.strip_prefix("```tool:") { if !tool_name.trim().starts_with("-result") { self.current_tool_name = tool_name.trim().to_string(); self.current_accumulator.clear(); self.state = ParserState::Collecting; } } } ParserState::Collecting => { if line.trim() == "```" { let parsed = parse_collected_json( &self.current_tool_name, &self.current_accumulator, ); results.push(parsed); self.state = ParserState::Text; } else { if !self.current_accumulator.is_empty() { self.current_accumulator.push('\n'); } self.current_accumulator.push_str(line); } } } } results } pub fn finish(mut self) -> Vec { let mut results = Vec::new(); if !self.leftover.is_empty() { // Treat leftover as a final line let line = self.leftover.clone(); match self.state { ParserState::Text => { let trimmed = line.trim(); if let Some(tool_name) = trimmed.strip_prefix("```tool:") { if !tool_name.trim().starts_with("-result") { self.current_tool_name = tool_name.trim().to_string(); self.current_accumulator.clear(); self.state = ParserState::Collecting; } } } ParserState::Collecting => { if line.trim() == "```" { let parsed = parse_collected_json( &self.current_tool_name, &self.current_accumulator, ); results.push(parsed); self.state = ParserState::Text; } else { if !self.current_accumulator.is_empty() { self.current_accumulator.push('\n'); } self.current_accumulator.push_str(&line); } } } } if self.state == ParserState::Collecting && !self.current_accumulator.is_empty() { results.push(parse_collected_json( &self.current_tool_name, &self.current_accumulator, )); } results } } pub fn parse_tool_call_blocks(text: &str) -> Vec { let mut parser = ToolParser::new(); let mut results = parser.ingest(text); results.extend(parser.finish()); results } pub fn parse_tool_call_stream<'a>(chunks: impl Iterator) -> Vec { let mut parser = ToolParser::new(); let mut results = Vec::new(); for chunk in chunks { results.extend(parser.ingest(chunk)); } results.extend(parser.finish()); results } fn parse_collected_json(tool_name: &str, json_str: &str) -> ParsedToolCall { match serde_json::from_str::(json_str) { Ok(payload) => { let call_id = payload .get("call_id") .and_then(|v| v.as_str()) .unwrap_or("missing_call_id") .to_string(); ParsedToolCall { tool_name: tool_name.to_string(), call_id, payload, raw_json: json_str.to_string(), } } Err(e) => ParsedToolCall { tool_name: tool_name.to_string(), call_id: "parse_error".to_string(), payload: serde_json::json!({ "error": "invalid_json", "details": e.to_string() }), raw_json: json_str.to_string(), }, } } pub fn format_tool_result( tool_name: &str, call_id: &str, status: &str, data: &serde_json::Value, ) -> String { let payload = serde_json::json!({ "call_id": call_id, "status": status, "data": data }); format!( "```tool-result:{}\n{}\n```", tool_name, serde_json::to_string_pretty(&payload).unwrap_or_default() ) } pub fn format_tool_error(tool_name: &str, call_id: &str, code: &str, message: &str) -> String { let data = serde_json::json!({ "code": code, "message": message }); format_tool_result(tool_name, call_id, "error", &data) } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolCallEnvelope { pub call_id: String, pub tool_name: String, pub arguments: serde_json::Value, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolResultEnvelope { pub call_id: String, pub tool_name: String, pub status: ToolResultStatus, pub data: Option, pub error: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ToolResultStatus { Success, Error, Streaming, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolErrorInfo { pub code: String, pub message: String, } /// Extended version tracking for tool definitions. /// Provides schema versioning, deprecation notices, and migration guidance. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolVersion { pub version: String, #[serde(default)] pub schema: serde_json::Value, #[serde(default)] pub deprecation_notes: Option, #[serde(default)] pub migration_guide: Option, } impl Default for ToolVersion { fn default() -> Self { Self { version: "1.0.0".to_string(), schema: serde_json::Value::Null, deprecation_notes: None, migration_guide: None, } } } /// Execution context for tool filtering. /// Determines which tools are available in which execution environment. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ToolContext { Reef, Krill, Both, } impl Default for ToolContext { fn default() -> Self { Self::Both } } /// Tool category classification for UI and filtering. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ToolCategory { Filesystem, Execution, Git, Compilation, Network, Workspace, Kanban, Documentation, Strategic, Agent, Other, } impl ToolCategory { pub fn as_str(&self) -> &'static str { match self { Self::Filesystem => "filesystem", Self::Execution => "execution", Self::Git => "git", Self::Compilation => "compilation", Self::Network => "network", Self::Workspace => "workspace", Self::Kanban => "kanban", Self::Documentation => "documentation", Self::Strategic => "strategic", Self::Agent => "agent", Self::Other => "other", } } } /// A composed workflow of multiple tool steps executed in sequence /// with dependency-based ordering. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolComposition { pub id: String, pub name: String, #[serde(default)] pub description: String, pub steps: Vec, #[serde(default = "default_composition_timeout_ms")] pub timeout_ms: u64, } const fn default_composition_timeout_ms() -> u64 { 60000 } /// A single step within a tool composition. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CompositionStep { pub tool: String, pub arguments: serde_json::Value, /// Indices of steps this step depends on (0-based). /// If None, the step has no dependencies and can run immediately. #[serde(default)] pub depends_on: Option>, /// Optional label for referencing step outputs #[serde(default)] pub label: Option, } /// Declares a dependency of one tool on another tool version. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolDependency { pub tool: String, #[serde(default = "default_dependency_version")] pub version: String, #[serde(default)] pub optional: bool, #[serde(default)] pub conflicts: Vec, } fn default_dependency_version() -> String { "1.0.0".to_string() } impl ToolDependency { pub fn new(tool: &str, version: &str) -> Self { Self { tool: tool.to_string(), version: version.to_string(), optional: false, conflicts: Vec::new(), } } pub fn optional(mut self) -> Self { self.optional = true; self } pub fn with_conflict(mut self, tool: &str) -> Self { self.conflicts.push(tool.to_string()); self } } macro_rules! tool { ($name:expr, $desc:expr, $cat:expr, $dangerous:expr, $approval:expr, $scoped:expr, $params:expr) => { ToolDefinition { id: $name.to_string(), name: $name.to_string(), description: $desc.to_string(), category: $cat.to_string(), parameters: $params, dangerous: $dangerous, requires_approval: $approval, version: "1.0.0".to_string(), project_scoped: $scoped, timeout_ms: default_timeout_ms(), deprecated: false, deprecation_message: String::new(), execution_context: ExecutionContext::default(), } }; ($name:expr, $desc:expr, $cat:expr, $dangerous:expr, $approval:expr, $scoped:expr, $params:expr, $timeout:expr) => { ToolDefinition { id: $name.to_string(), name: $name.to_string(), description: $desc.to_string(), category: $cat.to_string(), parameters: $params, dangerous: $dangerous, requires_approval: $approval, version: "1.0.0".to_string(), project_scoped: $scoped, timeout_ms: $timeout, deprecated: false, deprecation_message: String::new(), execution_context: ExecutionContext::default(), } }; } /// Trait for implementing custom tool plugins. /// Plugins can be registered at runtime to extend the tool ecosystem. pub trait ToolPlugin: Send + Sync { fn name(&self) -> &str; fn description(&self) -> &str; fn category(&self) -> &str; fn schema(&self) -> serde_json::Value; fn execute(&self, args: serde_json::Value) -> Result; } /// A marketplace listing for a published tool. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolListing { pub id: String, pub name: String, pub author: String, #[serde(default)] pub author_id: String, pub description: String, pub version: String, pub category: String, #[serde(default)] pub tags: Vec, #[serde(default)] pub downloads: u64, #[serde(default)] pub rating: f64, #[serde(default)] pub review_count: u64, #[serde(default)] pub source_url: Option, #[serde(default)] pub schema: serde_json::Value, #[serde(default)] pub published_at: String, } /// Results from a marketplace search. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MarketplaceSearchResults { pub listings: Vec, pub total_count: usize, pub page: usize, pub page_size: usize, } /// A documented example for a tool. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolExample { pub id: String, pub tool_name: String, pub title: String, #[serde(default)] pub description: String, pub arguments: serde_json::Value, #[serde(default)] pub expected_output: Option, #[serde(default)] pub tags: Vec, #[serde(default)] pub created_at: String, #[serde(default)] pub author: String, } /// A single test case for a tool. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolTest { pub name: String, pub input: serde_json::Value, #[serde(default)] pub expected_output: Option, #[serde(default)] pub expected_error: Option, #[serde(default = "default_test_timeout")] pub timeout_ms: u64, } const fn default_test_timeout() -> u64 { 30000 } /// A suite of tests for a specific tool. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolTestSuite { pub tool_name: String, pub tests: Vec, #[serde(default)] pub setup_actions: Vec, #[serde(default)] pub teardown_actions: Vec, } /// A setup or teardown action for a test suite. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TestAction { pub tool: String, pub arguments: serde_json::Value, } /// Result of running a single tool test. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolTestResult { pub test_name: String, pub passed: bool, #[serde(default)] pub actual_output: Option, #[serde(default)] pub error: Option, pub duration_ms: u64, } /// Results of running a full test suite. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolTestSuiteResult { pub tool_name: String, pub total: usize, pub passed: usize, pub failed: usize, pub results: Vec, pub total_duration_ms: u64, } /// Generated documentation for a tool. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolDocumentation { pub tool_name: String, pub description: String, pub category: String, pub version: String, pub markdown: String, pub parameters: serde_json::Value, #[serde(default)] pub dangerous: bool, #[serde(default)] pub requires_approval: bool, #[serde(default)] pub deprecated: bool, #[serde(default)] pub deprecation_message: String, #[serde(default)] pub examples: Vec, } /// Returns all built-in tool definitions across all categories. pub fn tool_definitions() -> Vec { vec![ // ======================== // Filesystem Tools // ======================== tool!( "read_file", "Read the content of a file with optional pagination", "filesystem", false, false, false, serde_json::json!({ "type": "object", "properties": { "path": { "type": "string", "description": "Path to the file to read" }, "offset": { "type": "number", "description": "Line number to start reading from (1-based)" }, "limit": { "type": "number", "description": "Maximum number of lines to read" } }, "required": ["path"] }) ), tool!( "write_file", "Create or overwrite a file with content", "filesystem", true, true, false, serde_json::json!({ "type": "object", "properties": { "path": { "type": "string", "description": "Path to the file to write" }, "content": { "type": "string", "description": "Content to write to the file" }, "create_dirs": { "type": "boolean", "description": "Create parent directories if they don't exist" } }, "required": ["path", "content"] }) ), tool!( "list_directory", "List directory contents with optional filtering", "filesystem", false, false, false, serde_json::json!({ "type": "object", "properties": { "path": { "type": "string", "description": "Path to the directory to list" }, "include_hidden": { "type": "boolean", "description": "Include hidden files (starting with .)" }, "recursive": { "type": "boolean", "description": "List subdirectories recursively" } }, "required": ["path"] }) ), tool!( "edit_file", "Edit file contents with line-based operations (replace, insert, delete)", "filesystem", true, true, false, serde_json::json!({ "type": "object", "properties": { "path": { "type": "string", "description": "Path to the file to edit" }, "edits": { "type": "array", "description": "Array of edit operations: {start_line, end_line (optional), new_content (optional), operation: 'replace'|'insert'|'delete'}", "items": { "type": "object", "properties": { "start_line": { "type": "number" }, "end_line": { "type": "number" }, "new_content": { "type": "string" }, "operation": { "type": "string", "enum": ["replace", "insert", "delete"] } }, "required": ["start_line", "operation"] } } }, "required": ["path", "edits"] }) ), tool!( "search_files", "Search for text patterns in files (grep-like)", "filesystem", false, false, false, serde_json::json!({ "type": "object", "properties": { "path": { "type": "string", "description": "Directory path to search in" }, "pattern": { "type": "string", "description": "Search pattern (regex supported)" }, "file_pattern": { "type": "string", "description": "File pattern to match (e.g., *.rs, *.ts)" } }, "required": ["path", "pattern"] }) ), // ======================== // Execution Tools // ======================== tool!( "bash", "Execute a shell command and capture output", "execution", true, true, false, serde_json::json!({ "type": "object", "properties": { "command": { "type": "string", "description": "The shell command to execute" }, "cwd": { "type": "string", "description": "Working directory for the command" }, "timeout_ms": { "type": "number", "description": "Timeout in milliseconds" } }, "required": ["command"] }) ), // ======================== // Git Tools // ======================== tool!( "git_status", "Show working tree status in a git repository", "git", false, false, false, serde_json::json!({ "type": "object", "properties": { "repo_path": { "type": "string", "description": "Path to the git repository" }, "short": { "type": "boolean", "description": "Use short format" } } }) ), tool!( "git_diff", "Show changes between commits, commit and working tree, etc.", "git", false, false, false, serde_json::json!({ "type": "object", "properties": { "repo_path": { "type": "string", "description": "Path to the git repository" }, "target": { "type": "string", "description": "Diff target (e.g., HEAD, HEAD~1, branch name)" }, "file": { "type": "string", "description": "Only show diff for specific file" } } }) ), tool!( "git_log", "Show commit history in a git repository", "git", false, false, false, serde_json::json!({ "type": "object", "properties": { "repo_path": { "type": "string", "description": "Path to the git repository" }, "max_count": { "type": "number", "description": "Maximum number of commits to show" } } }) ), tool!( "git_branch", "List or manage git branches", "git", false, false, false, serde_json::json!({ "type": "object", "properties": { "repo_path": { "type": "string", "description": "Path to the git repository" }, "all": { "type": "boolean", "description": "List all branches including remote" } } }) ), // ======================== // Build & Test Tools // ======================== tool!( "cargo_check", "Run cargo check on a Rust project to verify compilation", "build", false, false, false, serde_json::json!({ "type": "object", "properties": { "project_path": { "type": "string", "description": "Path to the Rust project (default: cwd)" }, "manifest_path": { "type": "string", "description": "Path to Cargo.toml" } } }) ), tool!( "npm_build", "Run npm build script in a JavaScript/TypeScript project", "build", false, false, false, serde_json::json!({ "type": "object", "properties": { "project_path": { "type": "string", "description": "Path to the npm project" }, "script": { "type": "string", "description": "Script to run (default: build)" } } }) ), tool!( "python_check", "Check Python syntax or run linting on a Python file", "build", false, false, false, serde_json::json!({ "type": "object", "properties": { "project_path": { "type": "string", "description": "Path to Python project" } }, "required": ["project_path"] }) ), tool!( "test_runner", "Run project tests with a test runner like pytest", "build", false, false, false, serde_json::json!({ "type": "object", "properties": { "project_path": { "type": "string", "description": "Path to the project" }, "test_command": { "type": "string", "description": "Command to run tests" }, "test_path": { "type": "string", "description": "Specific test path to run" } } }) ), // ======================== // Network Tools // ======================== tool!( "web_search", "Search the web for information using a search provider", "network", false, false, false, serde_json::json!({ "type": "object", "properties": { "query": { "type": "string", "description": "The search query" }, "provider": { "type": "string", "description": "Search provider (exa, google, bing)" }, "num_results": { "type": "number", "description": "Number of results to return" } }, "required": ["query"] }) ), tool!( "web_fetch", "Fetch and parse web pages from URLs", "network", false, false, false, serde_json::json!({ "type": "object", "properties": { "url": { "type": "string", "description": "URL to fetch" }, "format": { "type": "string", "description": "Response format: text, markdown, html" } }, "required": ["url"] }) ), tool!( "web_api", "Make HTTP API requests to external services", "network", false, false, false, serde_json::json!({ "type": "object", "properties": { "url": { "type": "string", "description": "API endpoint URL" }, "method": { "type": "string", "description": "HTTP method (GET, POST, PUT, DELETE, PATCH)" }, "body": { "type": "string", "description": "Request body (JSON)" } }, "required": ["url", "method"] }) ), // ======================== // Workspace Tools // ======================== tool!( "create_workspace", "Create an ephemeral workspace directory for isolated operations", "workspace", false, false, false, serde_json::json!({ "type": "object", "properties": { "name": { "type": "string", "description": "Optional workspace name (auto-generated if not provided)" }, "base_path": { "type": "string", "description": "Base path for workspaces (default: /tmp)" } } }) ), tool!( "delete_workspace", "Delete an ephemeral workspace and all its contents", "workspace", true, true, false, serde_json::json!({ "type": "object", "properties": { "path": { "type": "string", "description": "Path to the workspace to delete" }, "force": { "type": "boolean", "description": "Force deletion without confirmation" } }, "required": ["path"] }) ), tool!( "create_venv", "Create a Python virtual environment", "workspace", false, false, false, serde_json::json!({ "type": "object", "properties": { "path": { "type": "string", "description": "Path where to create the venv" }, "name": { "type": "string", "description": "Name of virtual environment (default: venv)" } }, "required": ["path"] }) ), tool!( "install_dependencies", "Install Python dependencies from requirements.txt or specified packages", "workspace", false, false, false, serde_json::json!({ "type": "object", "properties": { "path": { "type": "string", "description": "Path to project with requirements.txt" }, "package": { "type": "string", "description": "Specific package to install" } }, "required": ["path"] }) ), tool!( "workspace_info", "Get information and statistics about a workspace directory", "workspace", false, false, false, serde_json::json!({ "type": "object", "properties": { "path": { "type": "string", "description": "Path to the workspace" } }, "required": ["path"] }) ), // ======================== // Kanban Tools // ======================== tool!( "kanban_list_board", "List all columns and todos in the Kanban board", "kanban", false, false, true, serde_json::json!({ "type": "object", "properties": { "project_id": { "type": "string", "description": "Project ID" } }, "required": ["project_id"] }) ), tool!( "kanban_create_todo", "Create a new todo item in a Kanban column", "kanban", false, false, true, serde_json::json!({ "type": "object", "properties": { "project_id": { "type": "string", "description": "Project ID" }, "column_id": { "type": "string", "description": "Column ID to place the todo" }, "title": { "type": "string", "description": "Title of the todo" }, "description": { "type": "string", "description": "Optional description" }, "priority": { "type": "number", "description": "Priority (1-1000)" }, "deploy_agent": { "type": "boolean", "description": "Whether to deploy an agent for this todo" }, "agent_prompt": { "type": "string", "description": "Optional agent prompt if deploy_agent is true" }, "agent_task_details": { "type": "string", "description": "Optional agent task details" } }, "required": ["project_id", "column_id", "title"] }) ), tool!( "kanban_update_todo", "Update a todo item's title, description, status, or priority", "kanban", false, false, true, serde_json::json!({ "type": "object", "properties": { "project_id": { "type": "string", "description": "Project ID" }, "todo_id": { "type": "string", "description": "ID of the todo to update" }, "title": { "type": "string", "description": "New title" }, "description": { "type": "string", "description": "New description" }, "status": { "type": "string", "description": "New status (pending, in_progress, completed, blocked)" }, "priority": { "type": "number", "description": "New priority" } }, "required": ["project_id", "todo_id"] }) ), tool!( "kanban_delete_todo", "Delete a todo item from the Kanban board", "kanban", false, false, true, serde_json::json!({ "type": "object", "properties": { "project_id": { "type": "string", "description": "Project ID" }, "todo_id": { "type": "string", "description": "ID of the todo to delete" } }, "required": ["project_id", "todo_id"] }) ), tool!( "kanban_move_todo", "Move a todo to a different column or reorder it", "kanban", false, false, true, serde_json::json!({ "type": "object", "properties": { "project_id": { "type": "string", "description": "Project ID" }, "todo_id": { "type": "string", "description": "ID of the todo to move" }, "column_id": { "type": "string", "description": "Target column ID" }, "task_order": { "type": "number", "description": "New order position" } }, "required": ["project_id", "todo_id", "column_id"] }) ), tool!( "kanban_create_task", "Create a sub-task within a todo", "kanban", false, false, true, serde_json::json!({ "type": "object", "properties": { "project_id": { "type": "string", "description": "Project ID" }, "todo_id": { "type": "string", "description": "ID of the parent todo" }, "title": { "type": "string", "description": "Title of the task" }, "description": { "type": "string", "description": "Optional description" } }, "required": ["project_id", "todo_id", "title"] }) ), tool!( "kanban_update_task", "Update a sub-task's title, description, or status", "kanban", false, false, true, serde_json::json!({ "type": "object", "properties": { "project_id": { "type": "string", "description": "Project ID" }, "task_id": { "type": "string", "description": "ID of the task to update" }, "title": { "type": "string", "description": "New title" }, "description": { "type": "string", "description": "New description" }, "status": { "type": "string", "description": "New status" } }, "required": ["project_id", "task_id"] }) ), tool!( "kanban_add_tag", "Add a tag to a todo item", "kanban", false, false, true, serde_json::json!({ "type": "object", "properties": { "project_id": { "type": "string", "description": "Project ID" }, "todo_id": { "type": "string", "description": "ID of the todo" }, "tag_name": { "type": "string", "description": "Tag name" }, "tag_color": { "type": "string", "description": "Tag color (hex, default: #6366f1)" } }, "required": ["project_id", "todo_id", "tag_name"] }) ), tool!( "kanban_create_column", "Create a new column in the Kanban board", "kanban", false, false, true, serde_json::json!({ "type": "object", "properties": { "project_id": { "type": "string", "description": "Project ID" }, "name": { "type": "string", "description": "Column name" }, "column_order": { "type": "number", "description": "Display order" } }, "required": ["project_id", "name"] }) ), // ======================== // Documentation Tools // ======================== tool!( "document_file", "Analyze and generate documentation for a file", "documentation", false, false, true, serde_json::json!({ "type": "object", "properties": { "project_id": { "type": "string" }, "file_id": { "type": "string" } }, "required": ["project_id", "file_id"] }) ), tool!( "document_project", "Generate a comprehensive summary of all project files", "documentation", false, false, true, serde_json::json!({ "type": "object", "properties": { "project_id": { "type": "string" } }, "required": ["project_id"] }) ), tool!( "find_references", "Find all references to a symbol across the project", "documentation", false, false, true, serde_json::json!({ "type": "object", "properties": { "project_id": { "type": "string" }, "file_path": { "type": "string", "description": "Path to find all references for" }, "symbol_name": { "type": "string", "description": "Specific symbol name to find" }, "type": { "type": "string", "enum": ["function", "struct", "enum", "trait", "module", "type_alias"], "description": "Filter by symbol type" } }, "required": ["project_id"] }) ), tool!( "file_dependencies", "Get incoming or outgoing dependencies for a file", "documentation", false, false, true, serde_json::json!({ "type": "object", "properties": { "project_id": { "type": "string" }, "file_path": { "type": "string" }, "direction": { "type": "string", "enum": ["incoming", "outgoing", "both"], "description": "Direction of dependencies (default: both)" } }, "required": ["project_id", "file_path"] }) ), tool!( "documentation_tree", "Build a directory tree showing documentation coverage", "documentation", false, false, true, serde_json::json!({ "type": "object", "properties": { "project_id": { "type": "string" }, "root": { "type": "string", "description": "Optional root path to limit the tree" } }, "required": ["project_id"] }) ), tool!( "verify_documentation", "Verify documentation freshness and compute coverage stats", "documentation", false, false, true, serde_json::json!({ "type": "object", "properties": { "project_id": { "type": "string" } }, "required": ["project_id"] }) ), tool!( "document_folder", "Document all files within a folder", "documentation", false, false, true, serde_json::json!({ "type": "object", "properties": { "project_id": { "type": "string" }, "folder_path": { "type": "string", "description": "Relative folder path to document" } }, "required": ["project_id", "folder_path"] }) ), tool!( "store_file_doc", "Store documentation for a file", "documentation", false, false, true, serde_json::json!({ "type": "object", "properties": { "project_id": { "type": "string" }, "file_path": { "type": "string", "description": "Relative file path" }, "doc": { "type": "object", "description": "FileDoc-compatible documentation payload", "properties": { "summary": { "type": "string" }, "functions": { "type": "array", "items": { "type": "object" } }, "structs": { "type": "array", "items": { "type": "object" } } } } }, "required": ["project_id", "file_path", "doc"] }) ), tool!( "get_documentation_context", "Get the documentation context for a file", "documentation", false, false, true, serde_json::json!({ "type": "object", "properties": { "project_id": { "type": "string" }, "file_path": { "type": "string", "description": "Relative file path to get context for" } }, "required": ["project_id", "file_path"] }) ), // ======================== // File Tools // ======================== tool!( "list_files", "List all files in a project", "files", false, false, true, serde_json::json!({ "type": "object", "properties": { "project_id": { "type": "string" }, "path_prefix": { "type": "string", "description": "Optional path prefix to filter files" } }, "required": ["project_id"] }) ), // ======================== // Strategic Tools // ======================== tool!( "propose_strategic_item", "Propose a new high-level strategic goal or todo", "strategic", false, true, true, serde_json::json!({ "type": "object", "properties": { "project_id": { "type": "string", "description": "Project ID" }, "title": { "type": "string", "description": "Title of the strategic item" }, "description": { "type": "string", "description": "Detailed description" }, "rationale": { "type": "string", "description": "Why this item is important" }, "proposer_id": { "type": "string", "description": "Unique ID to prevent duplicates" } }, "required": ["project_id", "title", "description", "rationale", "proposer_id"] }) ), tool!( "change_planner_mode", "Suggest switching the Planner's personality mode", "strategic", false, true, true, serde_json::json!({ "type": "object", "properties": { "project_id": { "type": "string", "description": "Project ID" }, "mode": { "type": "string", "enum": ["cooperative", "critical", "red_team", "socratic"], "description": "The planner mode to switch to" }, "rationale": { "type": "string", "description": "Why this mode change is beneficial" } }, "required": ["project_id", "mode", "rationale"] }) ), tool!( "audit_assumptions", "List the user's current assumptions about the project and identify the weakest ones", "strategic", false, false, true, serde_json::json!({ "type": "object", "properties": { "project_id": { "type": "string", "description": "Project ID" } }, "required": ["project_id"] }) ), tool!( "identify_blind_spots", "Query project data for missing dependency chains, undocumented risks, and recurring blockers", "strategic", false, false, true, serde_json::json!({ "type": "object", "properties": { "project_id": { "type": "string", "description": "Project ID" } }, "required": ["project_id"] }) ), tool!( "check_dependencies", "Analyze todo dependency chains for circular or missing dependencies", "strategic", false, false, true, serde_json::json!({ "type": "object", "properties": { "project_id": { "type": "string", "description": "Project ID" } }, "required": ["project_id"] }) ), tool!( "evaluate_plan_risk", "Score the current plan for feasibility, edge-case coverage, and alignment", "strategic", false, false, true, serde_json::json!({ "type": "object", "properties": { "project_id": { "type": "string", "description": "Project ID" } }, "required": ["project_id"] }) ), tool!( "compare_project_patterns", "Compare kanban structures across projects to detect inconsistencies", "strategic", false, false, false, serde_json::json!({ "type": "object", "properties": { "project_ids": { "type": "array", "items": { "type": "string" }, "description": "Optional list of project IDs to compare" } } }) ), tool!( "find_similar_risks", "Search across projects for similar risk patterns", "strategic", false, false, false, serde_json::json!({ "type": "object", "properties": { "risk_type": { "type": "string", "description": "Type of risk (dependency, testing, security)" }, "project_ids": { "type": "array", "items": { "type": "string" }, "description": "Optional list of project IDs" } } }) ), // ======================== // Agent Tools // ======================== tool!( "report_completion", "Report completion status of a todo back to the Bridge", "agent", false, false, false, serde_json::json!({ "type": "object", "properties": { "todo_id": { "type": "string", "description": "ID of the todo being reported" }, "status": { "type": "string", "enum": ["done", "failed", "delegated", "needs_retry", "pending_approval"], "description": "Completion status" }, "summary": { "type": "string", "description": "Summary of what was done (max 3 sentences)" }, "artifact_refs": { "type": "array", "items": { "type": "string" }, "description": "File paths, test logs" }, "retry_plan": { "type": "string", "description": "Optional retry plan if status is needs_retry" }, "subtask_ids": { "type": "array", "items": { "type": "string" }, "description": "Subtask IDs if status is delegated" } }, "required": ["todo_id", "status"] }) ), tool!( "submit_batch_plan", "Submit a batch plan of mutations for pre-approval before execution", "agent", false, false, false, serde_json::json!({ "type": "object", "properties": { "plan_id": { "type": "string", "description": "Auto-generated plan ID" }, "steps": { "type": "array", "description": "List of mutation steps", "items": { "type": "object", "properties": { "tool": { "type": "string", "description": "Tool name (write_file, edit_file, bash)" }, "arguments": { "type": "object", "description": "Tool arguments" }, "expected_files": { "type": "array", "items": { "type": "string" }, "description": "Files expected to be modified" } }, "required": ["tool", "arguments"] }}, "rationale": { "type": "string", "description": "Why this batch of changes is needed" } }, "required": ["plan_id", "steps", "rationale"] }) ), ] } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[test] fn test_parse_single_block() { let text = r#" Some text before. ```tool:read_file { "call_id": "call_01", "path": "test.txt" } ``` Some text after. "#; let results = parse_tool_call_blocks(text); assert_eq!(results.len(), 1); assert_eq!(results[0].tool_name, "read_file"); assert_eq!(results[0].call_id, "call_01"); assert_eq!(results[0].payload["path"], "test.txt"); } #[test] fn test_parse_multiple_blocks() { let text = r#" ```tool:tool1 {"call_id": "c1"} ``` Middle text. ```tool:tool2 {"call_id": "c2"} ``` "#; let results = parse_tool_call_blocks(text); assert_eq!(results.len(), 2); assert_eq!(results[0].tool_name, "tool1"); assert_eq!(results[0].call_id, "c1"); assert_eq!(results[1].tool_name, "tool2"); assert_eq!(results[1].call_id, "c2"); } #[test] fn test_parse_error_json() { let text = r#" ```tool:bad_json { "invalid": ``` "#; let results = parse_tool_call_blocks(text); assert_eq!(results.len(), 1); assert_eq!(results[0].call_id, "parse_error"); assert_eq!(results[0].payload["error"], "invalid_json"); } #[test] fn test_parse_missing_call_id() { let text = r#" ```tool:no_id {"foo": "bar"} ``` "#; let results = parse_tool_call_blocks(text); assert_eq!(results.len(), 1); assert_eq!(results[0].call_id, "missing_call_id"); } #[test] fn test_streaming_parser() { let chunks = vec![ "Some text.\n```tool:", "my_tool\n", "{\"call_id\": \"st", "ream_01\"}\n", "```\nAnd more.", ]; let results = parse_tool_call_stream(chunks.into_iter()); assert_eq!(results.len(), 1); assert_eq!(results[0].tool_name, "my_tool"); assert_eq!(results[0].call_id, "stream_01"); } #[test] fn test_format_result() { let data = json!({"content": "hello world"}); let result = format_tool_result("read_file", "call_01", "success", &data); assert!(result.contains("```tool-result:read_file")); assert!(result.contains("call_01")); assert!(result.contains("success")); assert!(result.contains("hello world")); } #[test] fn test_parse_missing_closing_fence() { let text = r#" ```tool:orphan {"call_id": "call_01", "data": "never closed"}"#; let results = parse_tool_call_blocks(text); assert_eq!(results.len(), 1); assert_eq!(results[0].tool_name, "orphan"); assert_eq!(results[0].call_id, "call_01"); } #[test] fn test_parse_empty_input() { let results = parse_tool_call_blocks(""); assert!(results.is_empty()); let results = parse_tool_call_blocks(" \n \n "); assert!(results.is_empty()); } #[test] fn test_parse_mixed_content() { let text = r#"First, let me read the file. ```tool:read_file {"call_id": "call_01", "path": "test.txt"} ``` Now let me search. ```tool:search_files {"call_id": "call_02", "pattern": "TODO"} ``` Done with tools. "#; let results = parse_tool_call_blocks(text); assert_eq!(results.len(), 2); assert_eq!(results[0].tool_name, "read_file"); assert_eq!(results[0].call_id, "call_01"); assert_eq!(results[1].tool_name, "search_files"); assert_eq!(results[1].call_id, "call_02"); } #[test] fn test_parse_base64_content() { let text = r#" ```tool:write_file {"call_id": "call_01", "path": "out.bin", "content_base64": "SGVsbG8gV29ybGQ="} ``` "#; let results = parse_tool_call_blocks(text); assert_eq!(results.len(), 1); assert_eq!(results[0].tool_name, "write_file"); assert_eq!(results[0].call_id, "call_01"); assert_eq!(results[0].payload["content_base64"], "SGVsbG8gV29ybGQ="); } #[test] fn test_format_tool_error() { let result = format_tool_error("read_file", "call_01", "NOT_FOUND", "File not found"); assert!(result.contains("```tool-result:read_file")); assert!(result.contains("NOT_FOUND")); assert!(result.contains("File not found")); assert!(result.contains("\"status\": \"error\"")); } #[test] fn test_parse_tool_result_block_not_parsed() { let text = r#" ```tool-result:read_file {"call_id": "call_01", "status": "success", "data": {"content": "hello"}} ``` "#; let results = parse_tool_call_blocks(text); assert!( results.is_empty(), "tool-result: blocks should not be parsed by parse_tool_call_blocks" ); } #[test] fn test_tool_definition_to_openai_format() { let def = ToolDefinition { name: "test_tool".to_string(), description: "A test tool".to_string(), parameters: json!({"type": "object", "properties": {}}), ..Default::default() }; let openai = def.to_openai_format(); assert_eq!(openai["type"], "function"); assert_eq!(openai["function"]["name"], "test_tool"); assert_eq!(openai["function"]["description"], "A test tool"); } #[test] fn test_tool_definition_to_short_doc() { let def = ToolDefinition { name: "test_tool".to_string(), description: "A test tool".to_string(), parameters: json!({"type": "object"}), ..Default::default() }; let doc = def.to_short_doc(); assert!(doc.contains("test_tool")); assert!(doc.contains("A test tool")); } #[test] fn test_streaming_rejects_tool_result_block() { let chunks = vec![ "Some text.\n```tool-result:read_file\n", "{\"call_id\": \"call_01\"}\n", "```\nAnd more.", ]; let results = parse_tool_call_stream(chunks.into_iter()); assert!( results.is_empty(), "tool-result: blocks should be rejected by streaming parser" ); } #[test] fn test_streaming_rejects_tool_result_across_chunks() { let chunks = vec![ "```tool-", "result:read_file\n", "{\"call_id\": \"call_01\"}\n", "```\n", ]; let results = parse_tool_call_stream(chunks.into_iter()); assert!( results.is_empty(), "tool-result: split across chunks should be rejected" ); } #[test] fn test_parse_tool_not_result() { let text = r#" ```tool:result {"call_id": "call_01", "data": "this is a tool named 'result'"} ``` "#; let results = parse_tool_call_blocks(text); assert_eq!(results.len(), 1); assert_eq!(results[0].tool_name, "result"); assert_eq!(results[0].call_id, "call_01"); } #[test] fn test_streaming_tool_not_result() { let chunks = vec!["```tool:result\n", "{\"call_id\": \"call_01\"}\n", "```\n"]; let results = parse_tool_call_stream(chunks.into_iter()); assert_eq!(results.len(), 1); assert_eq!(results[0].tool_name, "result"); assert_eq!(results[0].call_id, "call_01"); } #[test] fn test_finish_rejects_tool_result() { // When finish() is called with a leftover that is a tool-result block, // it should not parse it. let mut parser = ToolParser::new(); let chunks = vec!["```tool-result:read_file\n{\"call_id\": \"call_01\"}\n```"]; let _ = parser.ingest(chunks[0]); let results = parser.finish(); assert!( results.is_empty(), "finish() should not parse tool-result blocks" ); } #[test] fn test_parse_tool_name_with_special_chars() { let text = r#" ```tool:my-tool_v2.with.dots {"call_id": "call_01"} ``` "#; let results = parse_tool_call_blocks(text); assert_eq!(results.len(), 1); assert_eq!(results[0].tool_name, "my-tool_v2.with.dots"); } #[test] fn test_parse_payload_with_newlines_and_unicode() { let text = "```tool:write_file\n{\"call_id\": \"call_01\", \"content\": \"line1\\nline2\\nunicode: 🎉\"}\n```"; let results = parse_tool_call_blocks(text); assert_eq!(results.len(), 1); assert_eq!(results[0].call_id, "call_01"); assert!(results[0].payload["content"] .as_str() .unwrap_or("") .contains("🎉")); } #[test] fn test_parse_block_immediately_followed_by_text() { let text = r#"```tool:read_file {"call_id": "call_01", "path": "test.txt"} ```And then text right after"#; let results = parse_tool_call_blocks(text); assert_eq!(results.len(), 1); assert_eq!(results[0].tool_name, "read_file"); } #[test] fn test_parse_tool_result_suffix_not_mistaken() { // "tool-result" as a suffix of a longer tool name should not be rejected let text = r#" ```tool:my-tool-result-processor {"call_id": "call_01"} ``` "#; let results = parse_tool_call_blocks(text); assert_eq!(results.len(), 1); assert_eq!(results[0].tool_name, "my-tool-result-processor"); assert_eq!(results[0].call_id, "call_01"); } #[test] fn test_tool_call_envelope_roundtrip() { let envelope = ToolCallEnvelope { call_id: "call_01".to_string(), tool_name: "test_tool".to_string(), arguments: json!({"key": "value"}), }; let json = serde_json::to_string(&envelope).unwrap(); let deserialized: ToolCallEnvelope = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.call_id, "call_01"); assert_eq!(deserialized.tool_name, "test_tool"); assert_eq!(deserialized.arguments["key"], "value"); } #[test] fn test_tool_result_envelope_roundtrip() { let envelope = ToolResultEnvelope { call_id: "call_01".to_string(), tool_name: "test_tool".to_string(), status: ToolResultStatus::Success, data: Some(json!({"result": "ok"})), error: None, }; let json = serde_json::to_string(&envelope).unwrap(); let deserialized: ToolResultEnvelope = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.call_id, "call_01"); assert!(matches!(deserialized.status, ToolResultStatus::Success)); assert_eq!(deserialized.data.unwrap()["result"], "ok"); } #[test] fn test_tool_definition_is_deprecated() { let mut def = ToolDefinition::default(); assert!(!def.is_deprecated()); def.deprecated = true; assert!(def.is_deprecated()); } #[test] fn test_tool_definition_deprecation_notice() { let mut def = ToolDefinition::default(); assert!(def.deprecation_notice().is_none()); def.deprecated = true; assert!(def.deprecation_notice().is_none()); def.deprecation_message = "Use new_tool instead".to_string(); assert_eq!(def.deprecation_notice(), Some("Use new_tool instead")); } #[test] fn test_tool_version_comparison() { let old = ToolDefinition { version: "1.0.0".to_string(), ..Default::default() }; let new = ToolDefinition { version: "2.0.0".to_string(), ..Default::default() }; assert!(new.is_newer_than(&old)); assert!(!old.is_newer_than(&new)); } #[test] fn test_filter_by_context() { let reef_tool = ToolDefinition { name: "reef_tool".to_string(), execution_context: ExecutionContext::Reef, ..Default::default() }; let krill_tool = ToolDefinition { name: "krill_tool".to_string(), execution_context: ExecutionContext::Krill, ..Default::default() }; let both_tool = ToolDefinition { name: "both_tool".to_string(), execution_context: ExecutionContext::Both, ..Default::default() }; let tools = vec![reef_tool, krill_tool, both_tool]; let reef_tools = ToolDefinition::filter_by_context(&tools, ExecutionContext::Reef); assert_eq!(reef_tools.len(), 2); assert!(reef_tools.iter().any(|t| t.name == "reef_tool")); assert!(reef_tools.iter().any(|t| t.name == "both_tool")); let krill_tools = ToolDefinition::filter_by_context(&tools, ExecutionContext::Krill); assert_eq!(krill_tools.len(), 2); assert!(krill_tools.iter().any(|t| t.name == "krill_tool")); assert!(krill_tools.iter().any(|t| t.name == "both_tool")); } #[test] fn test_filter_active_excludes_deprecated() { let active = ToolDefinition { name: "active".to_string(), deprecated: false, ..Default::default() }; let deprecated = ToolDefinition { name: "deprecated".to_string(), deprecated: true, ..Default::default() }; let tools = vec![active, deprecated]; let active_tools = ToolDefinition::filter_active(&tools); assert_eq!(active_tools.len(), 1); assert_eq!(active_tools[0].name, "active"); } #[test] fn test_tool_compatibility() { let tool_a = ToolDefinition { name: "my_tool".to_string(), ..Default::default() }; let tool_b = ToolDefinition { name: "my_tool".to_string(), ..Default::default() }; let tool_c = ToolDefinition { name: "other_tool".to_string(), ..Default::default() }; assert!(tool_a.is_compatible_with(&tool_b)); assert!(!tool_a.is_compatible_with(&tool_c)); let mut deprecated_tool = ToolDefinition { name: "my_tool".to_string(), deprecated: true, ..Default::default() }; assert!(!tool_a.is_compatible_with(&deprecated_tool)); deprecated_tool.deprecated = false; assert!(tool_a.is_compatible_with(&deprecated_tool)); } #[test] fn test_toolversion_default() { let version = ToolVersion::default(); assert_eq!(version.version, "1.0.0"); assert!(version.deprecation_notes.is_none()); assert!(version.migration_guide.is_none()); } #[test] fn test_execution_context_default() { assert_eq!( ExecutionContext::default(), ExecutionContext::Both ); } #[test] fn test_toolcategory_as_str() { assert_eq!(ToolCategory::Filesystem.as_str(), "filesystem"); assert_eq!(ToolCategory::Execution.as_str(), "execution"); assert_eq!(ToolCategory::Git.as_str(), "git"); assert_eq!(ToolCategory::Compilation.as_str(), "compilation"); assert_eq!(ToolCategory::Network.as_str(), "network"); assert_eq!(ToolCategory::Workspace.as_str(), "workspace"); assert_eq!(ToolCategory::Kanban.as_str(), "kanban"); assert_eq!(ToolCategory::Documentation.as_str(), "documentation"); assert_eq!(ToolCategory::Strategic.as_str(), "strategic"); assert_eq!(ToolCategory::Agent.as_str(), "agent"); assert_eq!(ToolCategory::Other.as_str(), "other"); } }