commons/src/tools.rs
Alex Emmet 212fea579f Syncing
2026-06-08 22:04:07 +02:00

2019 lines
69 KiB
Rust

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<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)]
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<ParsedToolCall> {
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<ParsedToolCall> {
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<ParsedToolCall> {
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<Item = &'a str>) -> Vec<ParsedToolCall> {
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::<serde_json::Value>(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<serde_json::Value>,
pub error: Option<ToolErrorInfo>,
}
#[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<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 {
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<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![
// ========================
// 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");
}
}