//! In-process tool registry for local dispatch. //! //! [`LocalRegistry`] maps [`ToolId`]s to type-erased //! [`ToolDyn`](crate::tool::ToolDyn) handles. //! Toolset finalization registers every config-enabled tool here and //! dispatch resolves handles via [`LocalRegistry::find`], so a call //! executes in-process without any wire round-trip. use std::sync::Arc; use indexmap::IndexMap; use parking_lot::RwLock; use crate::context::ListToolsContext; use crate::tool::{ArcTool, Tool}; use kigi_tool_protocol::ToolId; use kigi_tool_types::ToolDescription; /// In-process registry of tool handles. /// /// Mutations are concurrency-safe (`RwLock` on the entry map), so /// callers MAY hot-add or hot-remove tools while dispatch is in use. /// /// Entries use `RwLock` to preserve insertion order so that /// [`list_tools`](Self::list_tools) returns descriptions in the same /// order tools were registered (matching the config-defined order). #[derive(Clone, Default)] pub struct LocalRegistry { entries: Arc>>, } impl std::fmt::Debug for LocalRegistry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("LocalRegistry") .field("entries", &self.entries.read().len()) .finish() } } impl LocalRegistry { /// Construct an empty registry. pub fn new() -> Self { Self::default() } /// Register a typed [`Tool`] implementation by value. Subsequent /// registrations of the same id replace the previous handle and /// return the displaced handle for inspection / drop ordering. pub fn register(&self, tool: T) -> Option where T: Tool + 'static, { self.register_arc(Arc::new(tool)) } /// Register a typed [`Tool`] implementation already wrapped in `Arc`. pub fn register_arc(&self, tool: Arc) -> Option where T: Tool + 'static, { let id = tool.id(); self.entries.write().insert(id, tool as ArcTool) } /// Register a type-erased [`ToolDyn`](crate::tool::ToolDyn) directly. /// /// Use this for inherently dynamic tools (e.g. MCP tools retrieved /// from a registry as `Arc`) where the concrete type /// is not available. For native tools with a concrete type, prefer /// [`register`](Self::register). pub fn register_dyn(&self, tool: ArcTool) -> Option { let id = tool.id(); self.entries.write().insert(id, tool) } /// Resolve `tool_id` to its in-process handle, if registered. /// Returns a clone of the handle so the caller can dispatch without /// holding the lock across an await point. pub fn find(&self, tool_id: &ToolId) -> Option { self.entries.read().get(tool_id).cloned() } /// Drop the handle bound to `tool_id`. Returns `true` iff a /// matching entry was removed. pub fn unregister(&self, tool_id: &ToolId) -> bool { self.entries.write().shift_remove(tool_id).is_some() } /// Number of tools currently registered. pub fn len(&self) -> usize { self.entries.read().len() } /// `true` iff no tools are registered. pub fn is_empty(&self) -> bool { self.entries.read().is_empty() } /// `true` iff `tool_id` is currently registered. pub fn contains(&self, tool_id: &ToolId) -> bool { self.entries.read().contains_key(tool_id) } /// Descriptions of registered tools filtered by `should_list`. /// /// Returns descriptions in **insertion order** — the order tools /// were registered — so the caller sees the same ordering as the /// config-defined tool list. pub fn list_tools(&self, ctx: &ListToolsContext) -> Vec { self.entries .read() .values() .filter(|handle| handle.should_list(ctx)) .map(|handle| handle.description(ctx)) .collect() } }