diff --git a/src/lib.rs b/src/lib.rs index 8eae7cc..a58a4ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,5 +24,8 @@ pub use task::{Task, TaskResult}; pub use todo::ToDo; pub use tools::{ format_tool_error, format_tool_result, parse_tool_call_blocks, parse_tool_call_stream, - tool_definitions, ParsedToolCall, ToolDefinition, ToolParser, + tool_definitions, CompositionStep, ExecutionContext, MarketplaceSearchResults, ParsedToolCall, + TestAction, ToolComposition, ToolDefinition, ToolDependency, ToolDocumentation, ToolExample, + ToolListing, ToolParser, ToolPlugin, ToolTest, ToolTestResult, ToolTestSuite, + ToolTestSuiteResult, ToolVersion, }; diff --git a/src/tools.rs b/src/tools.rs index 7126886..8b833a1 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -1,5 +1,21 @@ 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)] @@ -22,6 +38,15 @@ pub struct ToolDefinition { /// 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 { @@ -41,6 +66,9 @@ impl Default for ToolDefinition { version: String::new(), project_scoped: false, timeout_ms: default_timeout_ms(), + deprecated: false, + deprecation_message: String::new(), + execution_context: ExecutionContext::default(), } } } @@ -77,6 +105,72 @@ impl ToolDefinition { } } } + + /// 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)] @@ -301,6 +395,149 @@ pub struct ToolErrorInfo { 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 { @@ -314,6 +551,9 @@ macro_rules! tool { 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) => { @@ -328,10 +568,157 @@ macro_rules! tool { 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![ @@ -1485,4 +1872,148 @@ Done with tools. 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"); + } }