AstroResearch/src/agent/tools/mod.rs
Asfmq f885c0a4a8 refactor: 服务层抽象下沉、异步锁全栈迁移、客户端韧性加固与移动端适配
- 服务层拆分:删除 api/helpers.rs,新增 citation/note/session/pipeline/paper/vision 独立服务模块
  - Agent 工具精简:paper_content+paper_outline 合并为 paper.rs,图片分析逻辑下沉至 services/vision
  - 并发模型升级:std::sync::{Mutex,RwLock} → tokio::sync::{Mutex,RwLock},消除 async
  上下文中的阻塞风险
  - 客户端加固:HTTP 客户端统一超时配置、ADS 429 / arXiv 503 自动重试、构造函数返回 Result
  - 启动安全:全局 panic hook 日志化、空密码拒绝启动、向量表维度不匹配需显式确认
  - CLI 扩展:构建完整 AppState 复用服务层,新增 Content/Outline/Citations/Search/Process 子命令
  - 前端:移动端汉堡菜单、侧栏滑出面板、引用星系触屏手势(单指拖拽/双指缩放)
2026-06-30 19:26:01 +08:00

757 lines
26 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/agent/tools/mod.rs
//
// 科研智能体工具集定义与实现。
// 每个工具遵循 AgentTool trait向大模型声明 JSON Schema 参数定义,
// 并在 execute 中调用已有的服务层完成实际业务操作。
//
// 按功能域拆分为子模块:
// filesystem/ — 文件 I/Oread、grep、glob、bash、write、edit
// astro/ — 天文科研(文献搜索/下载/解析、RAG、天体查询、笔记
// team.rs — 团队协作
// todo.rs — 任务规划
// compress.rs — 手动上下文压缩
use async_trait::async_trait;
use serde_json::json;
use std::sync::Arc;
use tokio::sync::RwLock;
use crate::agent::runtime::file_cache::FileStateCache;
use crate::agent::skills::SkillRegistry;
use crate::api::AppState;
use crate::clients::llm::ToolDefinition;
pub mod ask_user;
pub mod astro;
mod background;
mod compress;
mod filesystem;
pub mod memory;
pub mod persist;
pub mod search_history;
mod skill;
pub mod subagent;
mod team;
mod todo;
pub use ask_user::AskUserTool;
pub use astro::analyze_image::AnalyzeImageTool;
pub use astro::research::citation_network::GetCitationNetworkTool;
pub use astro::research::library_search::SearchLocalLibraryTool;
pub use astro::research::metadata::GetPaperMetadataTool;
pub use astro::research::note::SaveNoteTool;
pub use astro::research::paper::{GetPaperContentTool, GetPaperOutlineTool};
pub use astro::research::rag::RagSearchTool;
pub use astro::research::target::QueryTargetTool;
pub use astro::system::process::ProcessPaperTool;
pub use astro::system::search::SearchPapersTool;
pub use background::{BgTaskCheckTool, BgTaskRunTool};
pub use compress::CompressTool;
pub use filesystem::{
FileEditTool, FileWriteTool, GlobFilesTool, GrepFilesTool, ReadFileTool, RunBashTool,
};
pub use search_history::SearchHistoryTool;
pub use skill::LoadSkillTool;
pub use subagent::SubAgentTool;
pub use team::{CheckTeamInboxTool, SendTeammateMessageTool, SpawnTeammateTool, TeamBroadcastTool};
pub use todo::persist_tasks;
pub use todo::TodoWriteTool;
/// 工具执行上下文,封装全局共享状态
#[derive(Clone)]
pub struct ToolContext {
pub app_state: Arc<AppState>,
/// 当前会话 ID用于工具将数据关联到正确的 session
pub session_id: String,
/// 静默模式:子代理运行时为 true跳过用户权限提示
pub silent: bool,
/// 文件状态缓存(跨工具调用共享,用于 Read 去重)
pub read_file_state: Arc<std::sync::Mutex<FileStateCache>>,
/// 当前 SSE 发送通道(工具可通过此通道向用户推送中间事件)
pub sse_tx: Option<tokio::sync::mpsc::UnboundedSender<crate::agent::runtime::AgentStreamEvent>>,
/// 是否启用 LLM 思考模式(继承自父代理配置)
pub enable_thinking: bool,
/// 附加允许目录(扩展文件沙箱范围)
pub additional_allowed_dirs: Vec<String>,
/// 当前工具调用 ID工具可通过此 ID 将流式输出关联到自身)
pub tool_call_id: Option<String>,
/// 工具输出最大字符数(继承自 AgentConfig工具可据此截断输出
pub max_output_chars: usize,
}
impl ToolContext {
/// 创建标准上下文
pub fn new(app_state: Arc<AppState>) -> Self {
ToolContext {
app_state,
session_id: String::new(),
silent: false,
read_file_state: Arc::new(std::sync::Mutex::new(FileStateCache::new())),
sse_tx: None,
enable_thinking: false,
additional_allowed_dirs: Vec::new(),
tool_call_id: None,
max_output_chars: 4000,
}
}
/// 创建带共享文件缓存的上下文(用于 AgentRuntime 保持同一 cache 实例)
pub fn with_file_cache(
app_state: Arc<AppState>,
read_file_state: Arc<std::sync::Mutex<FileStateCache>>,
) -> Self {
ToolContext {
app_state,
session_id: String::new(),
silent: false,
read_file_state,
sse_tx: None,
enable_thinking: false,
additional_allowed_dirs: Vec::new(),
tool_call_id: None,
max_output_chars: 4000,
}
}
/// 设置 SSE 通道
pub fn with_sse_tx(
mut self,
tx: tokio::sync::mpsc::UnboundedSender<crate::agent::runtime::AgentStreamEvent>,
) -> Self {
self.sse_tx = Some(tx);
self
}
/// 设置会话 ID
pub fn with_session_id(mut self, id: String) -> Self {
self.session_id = id;
self
}
/// 设置思考模式
pub fn with_thinking(mut self, enable: bool) -> Self {
self.enable_thinking = enable;
self
}
/// 设置附加允许目录
pub fn with_additional_dirs(mut self, dirs: Vec<String>) -> Self {
self.additional_allowed_dirs = dirs;
self
}
/// 设置工具调用 ID用于工具的流式输出关联到自身
pub fn with_tool_call_id(mut self, id: String) -> Self {
self.tool_call_id = Some(id);
self
}
/// 设置最大输出字符数
pub fn with_max_output_chars(mut self, max_chars: usize) -> Self {
self.max_output_chars = max_chars;
self
}
/// 创建静默上下文(子代理使用)
pub fn silent(app_state: Arc<AppState>) -> Self {
ToolContext {
app_state,
session_id: String::new(),
silent: true,
read_file_state: Arc::new(std::sync::Mutex::new(FileStateCache::new())),
sse_tx: None,
enable_thinking: false,
additional_allowed_dirs: Vec::new(),
tool_call_id: None,
max_output_chars: 4000,
}
}
}
/// 工具执行结果
#[derive(Debug, Clone)]
pub struct ToolOutput {
/// 给大模型阅读的截断文本
pub content: String,
/// 是否为错误
pub is_error: bool,
/// 结构化元数据(给前端 Timeline 直接渲染)
pub metadata: serde_json::Value,
/// 跳过持久化到磁盘(用于 read_file 等已从磁盘读取内容的工具,避免级联持久化)
pub skip_persist: bool,
}
impl ToolOutput {
/// 创建成功结果
pub fn success(content: impl Into<String>, metadata: serde_json::Value) -> Self {
ToolOutput {
content: content.into(),
is_error: false,
metadata,
skip_persist: false,
}
}
/// 创建成功结果,跳过持久化
pub fn success_skip_persist(content: impl Into<String>, metadata: serde_json::Value) -> Self {
ToolOutput {
content: content.into(),
is_error: false,
metadata,
skip_persist: true,
}
}
/// 创建错误结果
pub fn error(msg: impl Into<String>) -> Self {
ToolOutput {
content: msg.into(),
is_error: true,
metadata: json!({}),
skip_persist: false,
}
}
}
/// 工具被中断时的行为策略
#[derive(Debug, Clone, PartialEq)]
pub enum InterruptBehavior {
/// 取消执行并返回错误(默认,适用于只读工具)
Cancel,
/// 阻塞中断信号直到执行完成(适用于有副作用的写入工具)
Block,
}
/// 权限规则来源
#[derive(Debug, Clone, PartialEq)]
pub enum PermissionRuleSource {
/// 环境变量加载AGENT_PERMISSIONS_*
Env,
/// 会话内动态添加API / Always Allow
Session,
}
impl std::fmt::Display for PermissionRuleSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PermissionRuleSource::Env => write!(f, "env"),
PermissionRuleSource::Session => write!(f, "session"),
}
}
}
/// 权限规则 — 工具自定义的权限限制
#[derive(Debug, Clone)]
pub enum PermissionRule {
/// 不可覆盖的拒绝
Deny {
tool_name: String,
reason: String,
source: PermissionRuleSource,
},
/// 允许
Allow {
tool_name: String,
source: PermissionRuleSource,
},
/// 需要用户确认
Ask {
tool_name: String,
message: String,
source: PermissionRuleSource,
},
}
/// 智能体工具 trait
#[async_trait]
pub trait AgentTool: Send + Sync {
/// 工具名称(与 LLM function calling 的 name 保持一致)
fn name(&self) -> &str;
/// 工具描述(告知 LLM 何时应该调用该工具)
fn description(&self) -> &str;
/// JSON Schema 格式的参数定义
fn parameters(&self) -> serde_json::Value;
/// 执行工具逻辑
async fn execute(&self, args: serde_json::Value, ctx: &ToolContext) -> ToolOutput;
// ── P1 优化:新增默认方法 ──
/// 中断行为策略。默认 Cancel — 可以安全中断。
fn interrupt_behavior(&self) -> InterruptBehavior {
InterruptBehavior::Cancel
}
/// 该工具是否支持并发安全执行。
/// 默认 false保守策略只读工具应覆写为 true。
fn is_concurrency_safe(&self, _args: &serde_json::Value) -> bool {
false
}
/// 工具自定义权限检查。默认无额外限制。
fn check_permissions(&self, _args: &serde_json::Value) -> Vec<PermissionRule> {
Vec::new()
}
/// 该工具错误时是否应中止兄弟并行执行。
/// 默认 false只读工具不触发。下载/解析类工具可覆写为 true。
fn causes_sibling_abort(&self) -> bool {
false
}
/// 带进度流式执行。默认委托给 execute()。
/// 长时间操作的工具可覆写以发送进度更新。
async fn execute_with_progress(
&self,
args: serde_json::Value,
ctx: &ToolContext,
_progress_tx: Option<&tokio::sync::mpsc::UnboundedSender<String>>,
) -> ToolOutput {
self.execute(args, ctx).await
}
// ── P3 优化:延迟加载与分类器支持 ──
/// 工具简要分类描述(~15 词),供 LLM 判断是否需要加载延迟工具。
/// 默认取 description 的第一句,截断到 100 字符。
fn classifier_summary(&self) -> String {
self.description()
.split('.')
.next()
.unwrap_or("")
.chars()
.take(100)
.collect()
}
/// 该工具是否为只读无副作用。auto-mode 分类器和权限系统可用此信息。
/// 默认 false保守——必须显式选择加入
fn is_readonly(&self) -> bool {
false
}
/// 该工具是否应延迟加载(不在初始 tool definitions 中,通过 tool_search 可发现)。
/// 默认 false常驻——始终在提示词中
fn defer_loading(&self) -> bool {
false
}
/// 工具所属分组。用于按功能域组织工具目录和模式过滤。
/// 默认返回 "general",天文科研工具应覆写为 "system" 或 "research"。
fn group(&self) -> &str {
"general"
}
}
/// 工具注册表,管理所有可用工具。
/// 内部使用 HashMap 实现 O(1) 按名查找,同时保留插入顺序供 definitions() 使用。
///
/// `definition_filter` 用于子代理最小权限:设置后 `definitions()` 仅返回白名单工具,
/// 但 `get()` 仍可查找所有工具(以便对未授权调用返回友好错误消息)。
pub struct ToolRegistry {
tools: std::collections::HashMap<String, Box<dyn AgentTool>>,
ordered_names: Vec<String>,
/// 可选的工具白名单(子代理最小权限)
definition_filter: Option<std::collections::HashSet<String>>,
/// 缓存的工具 schemasession 生命周期内有效,工具注册变更时失效)
schema_cache: Option<Vec<ToolDefinition>>,
/// schema 缓存版本号(递增使缓存失效)
schema_generation: u64,
}
// ── 工具注册辅助函数(消除重复代码) ──
/// 注册所有基础研究工具文件、文献、RAG、笔记等工具
fn add_base_tools(registry: &mut ToolRegistry, skill_registry: Arc<RwLock<SkillRegistry>>) {
let tools: Vec<Box<dyn AgentTool>> = vec![
Box::new(ReadFileTool),
Box::new(GrepFilesTool),
Box::new(GlobFilesTool),
Box::new(RunBashTool),
Box::new(FileWriteTool),
Box::new(FileEditTool),
// 系统级
Box::new(SearchPapersTool),
Box::new(ProcessPaperTool),
// 研究级
Box::new(SearchLocalLibraryTool),
Box::new(GetPaperMetadataTool),
Box::new(GetPaperOutlineTool),
Box::new(GetPaperContentTool),
Box::new(GetCitationNetworkTool),
Box::new(RagSearchTool),
Box::new(QueryTargetTool),
Box::new(SaveNoteTool),
Box::new(TodoWriteTool),
Box::new(CompressTool),
Box::new(AskUserTool),
Box::new(LoadSkillTool::new(skill_registry)),
Box::new(SubAgentTool::new()),
Box::new(SearchHistoryTool),
];
for tool in tools {
registry.ordered_names.push(tool.name().to_string());
registry.tools.insert(tool.name().to_string(), tool);
}
}
/// 注册后台任务工具bg_task_run, bg_task_check
fn add_background_tools(
registry: &mut ToolRegistry,
queue: Arc<crate::agent::background::BgNotificationQueue>,
) {
let run_tool = Box::new(BgTaskRunTool::new(queue.clone()));
let check_tool = Box::new(BgTaskCheckTool::new(queue));
registry.ordered_names.push(run_tool.name().to_string());
registry.tools.insert(run_tool.name().to_string(), run_tool);
registry.ordered_names.push(check_tool.name().to_string());
registry
.tools
.insert(check_tool.name().to_string(), check_tool);
}
/// 注册团队协作工具spawn_teammate, send_teammate_message, team_broadcast, check_team_inbox
fn add_team_tools(
registry: &mut ToolRegistry,
team_manager: Arc<tokio::sync::Mutex<Option<crate::agent::team::manager::TeamManager>>>,
) {
let team_tools: Vec<Box<dyn AgentTool>> = vec![
Box::new(SpawnTeammateTool::new(team_manager.clone())),
Box::new(SendTeammateMessageTool::new(team_manager.clone())),
Box::new(TeamBroadcastTool::new(team_manager.clone())),
Box::new(CheckTeamInboxTool::new(team_manager)),
];
for tool in team_tools {
registry.ordered_names.push(tool.name().to_string());
registry.tools.insert(tool.name().to_string(), tool);
}
}
impl ToolRegistry {
/// 创建空工具注册表(调用者通过 add_tool 手动添加工具)。
/// 用于受限场景(如记忆提取子代理只需要只读 + save_memory
pub fn empty() -> Self {
ToolRegistry {
tools: std::collections::HashMap::new(),
ordered_names: Vec::new(),
definition_filter: None,
schema_cache: None,
schema_generation: 0,
}
}
/// 创建默认工具注册表(包含全部科研工具,不含后台工具)
pub fn new(skill_registry: Arc<RwLock<SkillRegistry>>) -> Self {
Self::new_with_queue(None, skill_registry)
}
/// 创建工具注册表,可选注入后台通知队列以启用 bg_task_run/bg_task_check
pub fn new_with_queue(
queue: Option<Arc<crate::agent::background::BgNotificationQueue>>,
skill_registry: Arc<RwLock<SkillRegistry>>,
) -> Self {
let mut registry = ToolRegistry {
tools: std::collections::HashMap::new(),
ordered_names: Vec::new(),
definition_filter: None,
schema_cache: None,
schema_generation: 0,
};
add_base_tools(&mut registry, skill_registry);
if let Some(q) = queue {
add_background_tools(&mut registry, q);
}
registry.precompute_definitions();
registry
}
/// 创建包含团队工具的注册表
pub fn new_with_team(
queue: Option<Arc<crate::agent::background::BgNotificationQueue>>,
team_manager: Arc<tokio::sync::Mutex<Option<crate::agent::team::manager::TeamManager>>>,
skill_registry: Arc<RwLock<SkillRegistry>>,
) -> Self {
// 使用 new_with_queue 获取基础 + 后台工具,再添加团队工具
let mut registry = Self::new_with_queue(queue, skill_registry);
add_team_tools(&mut registry, team_manager);
registry.precompute_definitions();
registry
}
/// 动态添加工具(用于需要共享状态的工具,如 MemoryManager
pub fn add_tool(&mut self, tool: Box<dyn AgentTool>) {
let name = tool.name().to_string();
self.ordered_names.push(name.clone());
self.tools.insert(name, tool);
// 工具变更 → 使 schema 缓存失效
self.schema_cache = None;
self.schema_generation += 1;
}
/// 替换已存在的工具(保持名称在 ordered_names 中的位置不变)。
/// 如果工具不存在,行为等同于 add_tool。
pub fn replace_tool(&mut self, tool: Box<dyn AgentTool>) {
let name = tool.name().to_string();
if !self.tools.contains_key(&name) {
self.ordered_names.push(name.clone());
}
self.tools.insert(name, tool);
// 工具变更 → 使 schema 缓存失效
self.schema_cache = None;
self.schema_generation += 1;
}
/// 根据名称查找工具 (O(1))
pub fn get(&self, name: &str) -> Option<&dyn AgentTool> {
self.tools.get(name).map(|t| t.as_ref())
}
/// 设置工具定义白名单过滤器(子代理最小权限)。
/// 设置后 `definitions()` 仅返回白名单中的工具。
pub fn set_definition_filter(&mut self, allowed: Vec<String>) {
self.definition_filter = Some(allowed.into_iter().collect());
// 过滤器变更 → 使 schema 缓存失效
self.schema_cache = None;
self.schema_generation += 1;
}
/// 生成所有工具的 ToolDefinition 列表(用于发送给 LLM
/// 按名称字母序排序以保证跨调用的稳定性,提升 prompt cache 命中率。
/// 若设置了 definition_filter仅返回白名单中的工具。
///
/// 使用内部 schema 缓存:工具注册表不变时复用上次计算结果。
pub fn definitions(&self) -> Vec<ToolDefinition> {
// 缓存命中:直接返回
if let Some(ref cached) = self.schema_cache {
return cached.clone();
}
// 缓存未命中:重新计算
let values: Vec<&Box<dyn AgentTool>> = if let Some(filter) = &self.definition_filter {
self.tools
.iter()
.filter(|(name, _)| filter.contains(*name))
.map(|(_, tool)| tool)
.collect()
} else {
self.tools.values().collect()
};
let mut defs: Vec<_> = values
.iter()
.map(|t| ToolDefinition::new(t.name(), t.description(), t.parameters()))
.collect();
defs.sort_by(|a, b| a.function.name.cmp(&b.function.name));
// 缓存命中逻辑在上方 precompute_definitions() 中(&mut self预填充
// definitions() 持 &self 不可写入,仅读取缓存或当场计算返回。
// 调用方应在初始化时调用 precompute_definitions() 确保缓存已预热。
defs
}
/// 获取当前 schema 缓存版本号(用于外部检测工具是否变更)。
pub fn schema_generation(&self) -> u64 {
self.schema_generation
}
/// 预计算并缓存 tool definitions在 AgentRuntime 初始化时调用,
/// 此时拥有 &mut self可以安全写入缓存
pub fn precompute_definitions(&mut self) {
if self.schema_cache.is_some() {
return;
}
let values: Vec<&Box<dyn AgentTool>> = if let Some(filter) = &self.definition_filter {
self.tools
.iter()
.filter(|(name, _)| filter.contains(*name))
.map(|(_, tool)| tool)
.collect()
} else {
self.tools.values().collect()
};
let mut defs: Vec<_> = values
.iter()
.map(|t| ToolDefinition::new(t.name(), t.description(), t.parameters()))
.collect();
defs.sort_by(|a, b| a.function.name.cmp(&b.function.name));
self.schema_cache = Some(defs);
}
/// 设置工具白名单(子代理最小权限)。
/// 设置后 `definitions()` 仅暴露白名单中的工具给 LLM
/// 但 `get()` 仍可访问所有工具以便对未授权调用返回友好错误消息。
pub fn with_filter(mut self, allowed: &[&str]) -> Self {
self.definition_filter = Some(allowed.iter().map(|s| s.to_string()).collect());
self
}
/// 返回只常驻工具定义defer_loading() == false
/// 这是每次 LLM 调用的主工具列表。
pub fn resident_definitions(&self) -> Vec<ToolDefinition> {
self.tools
.values()
.filter(|t| !t.defer_loading())
.map(|t| ToolDefinition::new(t.name(), t.description(), t.parameters()))
.collect()
}
/// 返回延迟加载工具定义defer_loading() == true
pub fn deferred_definitions(&self) -> Vec<ToolDefinition> {
self.tools
.values()
.filter(|t| t.defer_loading())
.map(|t| ToolDefinition::new(t.name(), t.description(), t.parameters()))
.collect()
}
/// 构建紧凑的"可用工具目录"字符串,供系统提示词使用。
/// 格式: "tool_name: classifier_summary",按名称排序,延迟工具加 [deferred] 前缀。
pub fn tool_catalog(&self) -> String {
let mut entries: Vec<String> = self
.tools
.values()
.map(|t| {
let prefix = if t.defer_loading() { "[deferred]" } else { "" };
format!("{}{}: {}", prefix, t.name(), t.classifier_summary())
})
.collect();
entries.sort();
entries.join("\n")
}
/// 返回当前所有工具名称列表
pub fn tool_names(&self) -> Vec<String> {
self.ordered_names.clone()
}
/// 按分组名返回工具名称列表
pub fn tools_by_group(&self, group: &str) -> Vec<String> {
self.tools
.values()
.filter(|t| t.group() == group)
.map(|t| t.name().to_string())
.collect()
}
/// 构建按 group 分组的紧凑工具目录字符串。
/// 格式:
/// [system]
/// search_papers: ...
/// [research]
/// get_paper_metadata: ...
pub fn tool_catalog_grouped(&self) -> String {
let mut groups: std::collections::BTreeMap<&str, Vec<String>> =
std::collections::BTreeMap::new();
for tool in self.tools.values() {
let g = tool.group();
let prefix = if tool.defer_loading() {
"[deferred]"
} else {
""
};
groups.entry(g).or_default().push(format!(
"{}{}: {}",
prefix,
tool.name(),
tool.classifier_summary()
));
}
let mut out = String::new();
for (group, mut entries) in groups {
entries.sort();
out.push_str(&format!("[{}]\n", group));
for e in entries {
out.push_str(&format!("{}\n", e));
}
}
out
}
}
/// 截断文本到指定最大字符数
pub fn truncate_content(s: &str, max_chars: usize) -> String {
let char_count = s.chars().count();
if char_count <= max_chars {
s.to_string()
} else {
let truncated: String = s.chars().take(max_chars).collect();
format!(
"{}\n\n[... 内容已截断,共 {} 字符 ...]",
truncated, char_count
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_truncate_content_short() {
let text = "Hello, world!";
assert_eq!(truncate_content(text, 100), text);
}
#[test]
fn test_truncate_content_long() {
let text = "a".repeat(5000);
let result = truncate_content(&text, 100);
assert!(result.contains("内容已截断"));
assert!(result.contains("5000"));
}
#[test]
fn test_tool_output_success() {
let output = ToolOutput::success("ok", json!({"key": "value"}));
assert!(!output.is_error);
assert_eq!(output.content, "ok");
}
#[test]
fn test_tool_output_error() {
let output = ToolOutput::error("something went wrong");
assert!(output.is_error);
}
#[test]
fn test_tool_registry_definitions() {
let registry = ToolRegistry::new(Arc::new(RwLock::new(SkillRegistry::new(PathBuf::from(
"./skills",
)))));
let defs = registry.definitions();
assert_eq!(defs.len(), 22);
assert!(defs.iter().any(|d| d.function.name == "read_file"));
assert!(defs.iter().any(|d| d.function.name == "grep_files"));
assert!(defs.iter().any(|d| d.function.name == "glob_files"));
assert!(defs.iter().any(|d| d.function.name == "run_bash"));
assert!(defs.iter().any(|d| d.function.name == "file_write"));
assert!(defs.iter().any(|d| d.function.name == "file_edit"));
assert!(defs.iter().any(|d| d.function.name == "search_papers"));
assert!(defs.iter().any(|d| d.function.name == "process_paper"));
assert!(defs
.iter()
.any(|d| d.function.name == "search_local_library"));
assert!(defs.iter().any(|d| d.function.name == "get_paper_metadata"));
assert!(defs.iter().any(|d| d.function.name == "get_paper_outline"));
assert!(defs.iter().any(|d| d.function.name == "get_paper_content"));
assert!(defs
.iter()
.any(|d| d.function.name == "get_citation_network"));
assert!(defs.iter().any(|d| d.function.name == "rag_search"));
assert!(defs.iter().any(|d| d.function.name == "query_target"));
assert!(defs.iter().any(|d| d.function.name == "save_note"));
assert!(defs.iter().any(|d| d.function.name == "todo_write"));
assert!(defs.iter().any(|d| d.function.name == "compress_context"));
assert!(defs.iter().any(|d| d.function.name == "load_skill"));
assert!(defs.iter().any(|d| d.function.name == "subagent"));
}
#[test]
fn test_tool_registry_get() {
let registry = ToolRegistry::new(Arc::new(RwLock::new(SkillRegistry::new(PathBuf::from(
"./skills",
)))));
assert!(registry.get("search_papers").is_some());
assert!(registry.get("nonexistent").is_none());
}
}