This commit is contained in:
Alex Emmet 2026-06-08 22:04:07 +02:00
commit 212fea579f
2 changed files with 535 additions and 1 deletions

View file

@ -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<ToolDefinition> {
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<ToolDefinition> {
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<u64> {
v.split('.')
.filter_map(|part| part.parse::<u64>().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<String>,
#[serde(default)]
pub migration_guide: Option<String>,
}
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<CompositionStep>,
#[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<Vec<usize>>,
/// Optional label for referencing step outputs
#[serde(default)]
pub label: Option<String>,
}
/// 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<String>,
}
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<serde_json::Value, String>;
}
/// 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<String>,
#[serde(default)]
pub downloads: u64,
#[serde(default)]
pub rating: f64,
#[serde(default)]
pub review_count: u64,
#[serde(default)]
pub source_url: Option<String>,
#[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<ToolListing>,
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_json::Value>,
#[serde(default)]
pub tags: Vec<String>,
#[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_json::Value>,
#[serde(default)]
pub expected_error: Option<String>,
#[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<ToolTest>,
#[serde(default)]
pub setup_actions: Vec<TestAction>,
#[serde(default)]
pub teardown_actions: Vec<TestAction>,
}
/// 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_json::Value>,
#[serde(default)]
pub error: Option<String>,
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<ToolTestResult>,
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<ToolExample>,
}
/// Returns all built-in tool definitions across all categories.
pub fn tool_definitions() -> Vec<ToolDefinition> {
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");
}
}