2019 lines
69 KiB
Rust
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");
|
|
}
|
|
}
|