pub mod accounts;
pub mod bindings;
pub mod caatinga;
pub mod docs;
pub mod events;
pub mod file;
pub mod invoke;
pub mod mainnet;
pub mod party;
pub mod paths;
pub mod persona;
pub mod plugin;
pub mod project;
pub mod search;
pub mod skill;
pub mod test;
pub mod update;
use async_trait::async_trait;
use serde_json::Value;
pub fn is_contract_id(candidate: &str) -> bool {
candidate.len() == 56
&& candidate.starts_with('C')
&& candidate
.bytes()
.all(|b| b.is_ascii_uppercase() || b.is_ascii_digit())
}
#[async_trait]
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn input_schema(&self) -> Value;
async fn execute(&self, input: Value) -> Result<String, String>;
}
pub struct ToolRegistry {
tools: Vec<Box<dyn Tool>>,
}
impl ToolRegistry {
pub fn new() -> Self {
Self { tools: Vec::new() }
}
pub fn register(&mut self, tool: Box<dyn Tool>) {
self.tools.push(tool);
}
pub fn try_register(&mut self, tool: Box<dyn Tool>) -> Result<(), String> {
if self.get_tool(tool.name()).is_some() {
return Err(format!(
"tool '{}' is already registered and was skipped",
tool.name()
));
}
self.tools.push(tool);
Ok(())
}
pub fn get_tool(&self, name: &str) -> Option<&dyn Tool> {
self.tools
.iter()
.find(|t| t.name() == name)
.map(|t| t.as_ref())
}
pub fn definitions(&self) -> Vec<crate::agent::ToolDefinition> {
self.tools
.iter()
.map(|t| crate::agent::ToolDefinition {
name: t.name().to_string(),
description: t.description().to_string(),
input_schema: t.input_schema(),
})
.collect()
}
pub async fn execute(&self, name: &str, input: Value) -> Result<String, String> {
let tool = self
.get_tool(name)
.ok_or_else(|| format!("Unknown tool: {}", name))?;
tool.execute(input).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
struct SlowTool;
#[async_trait]
impl Tool for SlowTool {
fn name(&self) -> &str {
"slow"
}
fn description(&self) -> &str {
"sleeps"
}
fn input_schema(&self) -> Value {
json!({"type": "object"})
}
async fn execute(&self, _input: Value) -> Result<String, String> {
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
Ok("done".to_string())
}
}
#[tokio::test(flavor = "current_thread")]
async fn a_slow_tool_does_not_stall_the_render_loop() {
let mut registry = ToolRegistry::new();
registry.register(Box::new(SlowTool));
let frames = Arc::new(AtomicUsize::new(0));
let ticker = {
let frames = frames.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
frames.fetch_add(1, Ordering::Relaxed);
}
})
};
let result = registry.execute("slow", json!({})).await.unwrap();
ticker.abort();
assert_eq!(result, "done");
assert!(
frames.load(Ordering::Relaxed) > 5,
"render loop only advanced {} frames during a 300ms tool",
frames.load(Ordering::Relaxed)
);
}
#[tokio::test(flavor = "current_thread")]
async fn npx_check_does_not_spawn_a_process() {
let start = std::time::Instant::now();
for _ in 0..50 {
let _ = crate::tools::caatinga::check_npx_available();
}
assert!(
start.elapsed() < std::time::Duration::from_millis(250),
"50 npx checks took {:?}; that is process-spawn territory",
start.elapsed()
);
}
}