Skip to main content

rig_agent/tool/
server.rs

1use std::{collections::BTreeSet, sync::Arc};
2
3#[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
4use std::collections::HashMap;
5
6use indexmap::IndexMap;
7use tokio::sync::RwLock;
8
9#[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
10use crate::tool::ErasedTool;
11
12use crate::{
13    completion::{CompletionError, ToolDefinition},
14    tool::{
15        DynamicTool, PortableDynamicTool, RegisteredTool, Tool, ToolContext, ToolDispatch,
16        ToolResult, ToolSet, dispatch_tool,
17    },
18};
19use rig_core::vector_store::{
20    VectorSearchRequest, VectorStoreError, VectorStoreIndexDyn, request::Filter,
21};
22
23/// One turn's provider definitions and the exact registry entries behind them.
24///
25/// Registration changes after this snapshot is built take effect on the next
26/// turn. Calls from the current turn dispatch through these pinned handles, so
27/// the implementation cannot drift from the schema the provider received.
28#[derive(Clone)]
29pub(crate) struct ToolRegistrySnapshot {
30    definitions: Vec<ToolDefinition>,
31    tools: IndexMap<String, RegisteredTool>,
32}
33
34impl ToolRegistrySnapshot {
35    fn new(tools: IndexMap<String, RegisteredTool>) -> Self {
36        let definitions = tools
37            .iter()
38            .map(|(name, tool)| tool.definition_with_name(name.clone()))
39            .collect();
40        Self { definitions, tools }
41    }
42
43    /// Provider-facing definitions in the same order as their pinned handles.
44    pub(crate) fn definitions(&self) -> &[ToolDefinition] {
45        &self.definitions
46    }
47
48    /// Narrow both provider exposure and dispatch to one per-turn allow-list.
49    pub(crate) fn retain_names(&mut self, names: &BTreeSet<String>) {
50        self.definitions
51            .retain(|definition| names.contains(&definition.name));
52        self.tools.retain(|name, _| names.contains(name));
53    }
54
55    /// Dispatch through the exact implementation advertised for this turn.
56    pub(crate) async fn dispatch(
57        &self,
58        tool_name: &str,
59        args: &str,
60        context: &ToolContext,
61    ) -> ToolDispatch {
62        let tool = self.tools.get(tool_name).cloned();
63        dispatch_tool(tool_name, args.to_string(), tool, context).await
64    }
65}
66
67/// Shared state behind a `ToolServerHandle`.
68struct ToolServerState {
69    /// Vector indexes used to select retrieval-only tools for each prompt.
70    retrieval_indexes: Vec<(usize, Arc<dyn VectorStoreIndexDyn + Send + Sync>)>,
71    /// The authoritative ordered registry for execution and exposure.
72    toolset: ToolSet,
73    /// Generation tokens for registrations managed by MCP client handlers.
74    /// A normal registration clears the token, preventing a stale handler
75    /// refresh from replacing or removing the newer tool.
76    #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
77    managed_generations: HashMap<String, ManagedToolToken>,
78}
79
80#[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
81impl ToolServerState {
82    /// Remove remote registrations whose transport can no longer accept calls.
83    /// In-process tools use the default live state, while both handler-managed
84    /// and directly registered MCP tools report their transport state.
85    fn retire_disconnected_tools(&mut self) {
86        let disconnected = self
87            .toolset
88            .tools
89            .keys()
90            .filter(|name| self.toolset.get(name).is_none_or(|tool| !tool.is_live()))
91            .cloned()
92            .collect::<Vec<_>>();
93
94        for name in disconnected {
95            self.toolset.delete_tool(&name);
96            self.managed_generations.remove(&name);
97            tracing::debug!(tool_name = %name, "retired disconnected MCP tool registration");
98        }
99    }
100}
101
102/// Opaque identity for one MCP-managed registry generation.
103#[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
104#[derive(Clone, Debug)]
105pub(crate) struct ManagedToolToken(Arc<()>);
106
107#[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
108impl ManagedToolToken {
109    fn new() -> Self {
110        Self(Arc::new(()))
111    }
112}
113
114#[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
115impl PartialEq for ManagedToolToken {
116    fn eq(&self, other: &Self) -> bool {
117        Arc::ptr_eq(&self.0, &other.0)
118    }
119}
120
121#[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
122impl Eq for ManagedToolToken {}
123
124/// Builder for constructing a [`ToolServerHandle`].
125///
126/// Accumulates tools and configuration, then produces a shared handle via
127/// [`run()`](ToolServer::run).
128pub struct ToolServer {
129    retrieval_indexes: Vec<(usize, Arc<dyn VectorStoreIndexDyn + Send + Sync>)>,
130    toolset: ToolSet,
131}
132
133impl Default for ToolServer {
134    fn default() -> Self {
135        Self::new()
136    }
137}
138
139impl ToolServer {
140    pub fn new() -> Self {
141        Self {
142            retrieval_indexes: Vec::new(),
143            toolset: ToolSet::default(),
144        }
145    }
146
147    pub(crate) fn add_tools(mut self, tools: ToolSet) -> Self {
148        self.toolset = tools;
149        self
150    }
151
152    pub(crate) fn add_retrieval_indexes(
153        mut self,
154        indexes: Vec<(usize, Arc<dyn VectorStoreIndexDyn + Send + Sync>)>,
155    ) -> Self {
156        self.retrieval_indexes = indexes;
157        self
158    }
159
160    /// Add a static tool to the agent. Re-registering an existing name
161    /// replaces the implementation (last wins) and keeps its position.
162    pub fn tool(mut self, tool: impl Tool + 'static) -> Self {
163        self.toolset.add_tool(tool);
164        self
165    }
166
167    /// Add a runtime-defined tool. Re-registering an existing name replaces
168    /// the implementation and keeps its position.
169    pub fn dynamic_tool(mut self, tool: DynamicTool) -> Self {
170        self.toolset.add_dynamic_tool(tool);
171        self
172    }
173
174    /// Add a context-free dynamic tool through the classic registry adapter.
175    pub fn portable_dynamic_tool(mut self, tool: PortableDynamicTool) -> Self {
176        self.toolset.add_portable_dynamic_tool(tool);
177        self
178    }
179
180    /// Add an MCP tool (from `rmcp`) to the agent, bounded by
181    /// [`DEFAULT_MCP_TOOL_TIMEOUT`](crate::tool::rmcp::DEFAULT_MCP_TOOL_TIMEOUT)
182    /// (see issue #1914). Use [`rmcp_tool_with_timeout`](Self::rmcp_tool_with_timeout)
183    /// to change or disable it.
184    #[cfg_attr(docsrs, doc(cfg(feature = "rmcp")))]
185    #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
186    pub fn rmcp_tool(self, tool: rmcp::model::Tool, client: rmcp::service::ServerSink) -> Self {
187        self.rmcp_tool_with_timeout(tool, client, crate::tool::rmcp::DEFAULT_MCP_TOOL_TIMEOUT)
188    }
189
190    /// Add an MCP tool (from `rmcp`) with a per-call timeout (see issue #1914).
191    ///
192    /// Pass a [`Duration`](std::time::Duration) to bound the call, or `None` to
193    /// disable the timeout (unbounded).
194    #[cfg_attr(docsrs, doc(cfg(feature = "rmcp")))]
195    #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
196    pub fn rmcp_tool_with_timeout(
197        mut self,
198        tool: rmcp::model::Tool,
199        client: rmcp::service::ServerSink,
200        timeout: impl Into<Option<std::time::Duration>>,
201    ) -> Self {
202        use crate::tool::rmcp::McpTool;
203        self.toolset.add_erased(Arc::new(
204            McpTool::from_mcp_server(tool, client).with_timeout(timeout),
205        ));
206        self
207    }
208
209    /// Configure tools retrieved from a vector index for each prompt.
210    pub fn retrieved_tools(
211        mut self,
212        sample: usize,
213        index: impl VectorStoreIndexDyn + Send + Sync + 'static,
214        toolset: ToolSet,
215    ) -> Self {
216        self.retrieval_indexes.push((sample, Arc::new(index)));
217        self.toolset.add_retrievable_tools(toolset);
218        self
219    }
220
221    /// Consume the builder and return a shared [`ToolServerHandle`].
222    pub fn run(self) -> ToolServerHandle {
223        ToolServerHandle(Arc::new(RwLock::new(ToolServerState {
224            retrieval_indexes: self.retrieval_indexes,
225            toolset: self.toolset,
226            #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
227            managed_generations: HashMap::new(),
228        })))
229    }
230}
231
232/// A cheaply-cloneable handle to the shared tool server state.
233///
234/// All operations acquire locks directly on the underlying state.
235/// Multiple handles (e.g. across agents) can share the same state
236/// without channel-based message routing.
237#[derive(Clone)]
238pub struct ToolServerHandle(Arc<RwLock<ToolServerState>>);
239
240impl ToolServerHandle {
241    /// Register a new static tool. Re-registering an existing name replaces
242    /// the implementation (last wins) and keeps its position.
243    pub async fn add_tool<T>(&self, tool: T)
244    where
245        T: Tool + 'static,
246    {
247        let mut state = self.0.write().await;
248        let _name = state.toolset.add_tool(tool);
249        #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
250        state.managed_generations.remove(&_name);
251    }
252
253    /// Register a runtime-defined static tool.
254    pub async fn add_dynamic_tool(&self, tool: DynamicTool) {
255        let mut state = self.0.write().await;
256        let _name = state.toolset.add_dynamic_tool(tool);
257        #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
258        state.managed_generations.remove(&_name);
259    }
260
261    /// Register a context-free dynamic tool through the classic adapter.
262    pub async fn add_portable_dynamic_tool(&self, tool: PortableDynamicTool) {
263        let mut state = self.0.write().await;
264        let _name = state.toolset.add_portable_dynamic_tool(tool);
265        #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
266        state.managed_generations.remove(&_name);
267    }
268
269    #[cfg(all(feature = "rmcp", test))]
270    pub(crate) async fn add_erased_tool(&self, tool: Arc<dyn ErasedTool>) {
271        let mut state = self.0.write().await;
272        let name = state.toolset.add_erased(tool);
273        state.managed_generations.remove(&name);
274    }
275
276    /// Atomically install the initial tools owned by one MCP handler.
277    /// Initial connection retains the registry's last-registration-wins policy.
278    #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
279    pub(crate) async fn add_managed_erased_tools(
280        &self,
281        tools: Vec<Arc<dyn ErasedTool>>,
282    ) -> HashMap<String, ManagedToolToken> {
283        let mut state = self.0.write().await;
284        let mut managed = HashMap::with_capacity(tools.len());
285
286        for tool in tools {
287            // The initial list fetch can complete just before the transport
288            // closes. Avoid installing a registration that can never execute.
289            if !tool.is_live() {
290                tracing::debug!(
291                    tool_name = %tool.name(),
292                    "ignored initial registration from disconnected MCP owner"
293                );
294                continue;
295            }
296
297            let name = state.toolset.add_erased(tool);
298            let token = ManagedToolToken::new();
299            state
300                .managed_generations
301                .insert(name.clone(), token.clone());
302            managed.insert(name, token);
303        }
304
305        managed
306    }
307
308    /// Atomically reconcile one handler's MCP registrations with a refreshed
309    /// tool list. Existing names are changed only when their expected generation
310    /// remains current; newer local or peer-handler registrations win.
311    #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
312    pub(crate) async fn reconcile_managed_erased_tools(
313        &self,
314        mut expected: HashMap<String, ManagedToolToken>,
315        tools: Vec<Arc<dyn ErasedTool>>,
316    ) -> HashMap<String, ManagedToolToken> {
317        let mut state = self.0.write().await;
318        let mut refreshed = HashMap::with_capacity(tools.len());
319        let mut managed_order = Vec::with_capacity(tools.len());
320        let mut seen = std::collections::HashSet::with_capacity(tools.len());
321
322        // A generation only protects a live owner. MCP service shutdown closes
323        // the sink held by its registered tools, so retire those generations
324        // before deciding whether another handler may reclaim a name. Local
325        // in-process registrations stay live; directly registered MCP tools are
326        // also retired when their sink closes.
327        state.retire_disconnected_tools();
328
329        for tool in tools {
330            // A refresh that raced with service shutdown may already have
331            // fetched definitions before the transport closed. Do not let
332            // that stale refresh recreate an owner we just retired.
333            if !tool.is_live() {
334                tracing::debug!(
335                    tool_name = %tool.name(),
336                    "ignored registration from disconnected MCP owner"
337                );
338                continue;
339            }
340
341            let name = tool.name();
342            if !seen.insert(name.clone()) {
343                tracing::warn!(tool_name = %name, "ignoring duplicate MCP tool definition");
344                continue;
345            }
346            let present = state.toolset.contains(&name);
347            let may_register = match expected.remove(&name) {
348                Some(token) if present => state.managed_generations.get(&name) == Some(&token),
349                // A stale expected token protects a live newer registration,
350                // not an empty slot. Once the competitor disappears, this full
351                // server snapshot must converge in one reconciliation.
352                Some(_) => true,
353                None => !present,
354            };
355
356            if may_register {
357                state.toolset.add_erased(tool);
358                let token = ManagedToolToken::new();
359                state
360                    .managed_generations
361                    .insert(name.clone(), token.clone());
362                refreshed.insert(name.clone(), token);
363                managed_order.push(name);
364            } else {
365                tracing::debug!(
366                    tool_name = name,
367                    "MCP refresh left a newer same-name registration intact"
368                );
369            }
370        }
371
372        for (name, token) in expected {
373            if state.managed_generations.get(&name) == Some(&token) {
374                state.toolset.delete_tool(&name);
375                state.managed_generations.remove(&name);
376            }
377        }
378
379        // A full MCP list is ordered. Move only entries this handler actually
380        // owns to the end in that order, matching remove/re-register semantics;
381        // live local or peer-handler competitors retain their relative slots.
382        let mut ordered_entries = Vec::with_capacity(managed_order.len());
383        for name in managed_order {
384            if let Some(entry) = state.toolset.tools.shift_remove_entry(&name) {
385                ordered_entries.push(entry);
386            }
387        }
388        for (name, registration) in ordered_entries {
389            state.toolset.tools.insert(name, registration);
390        }
391
392        refreshed
393    }
394
395    /// Merge an entire toolset into the server in registration order.
396    /// Existing names are replaced (last wins) and keep their position.
397    pub async fn append_toolset(&self, toolset: ToolSet) {
398        let mut state = self.0.write().await;
399        #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
400        let names = toolset.tools.keys().cloned().collect::<Vec<_>>();
401        state.toolset.add_tools(toolset);
402        #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
403        for name in names {
404            state.managed_generations.remove(&name);
405        }
406    }
407
408    /// Remove a tool by name.
409    pub async fn remove_tool(&self, tool_name: &str) {
410        let mut state = self.0.write().await;
411        state.toolset.delete_tool(tool_name);
412        #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
413        state.managed_generations.remove(tool_name);
414    }
415
416    /// Look up and execute a tool through the canonical structured path.
417    ///
418    /// The implementation handle is cloned under a brief read lock, so a long
419    /// execution never blocks registration changes. The tool receives one
420    /// snapshot of the supplied inbound values. Its result metadata is
421    /// published back to `context`, while mutations to its inbound snapshot are
422    /// discarded.
423    pub async fn execute(
424        &self,
425        tool_name: &str,
426        args: &str,
427        context: &mut ToolContext,
428    ) -> ToolResult {
429        context.clear_dispatch_result();
430        let ToolDispatch {
431            result,
432            context: dispatch_context,
433        } = self.dispatch(tool_name, args, context).await;
434        context.accept_dispatch_result(dispatch_context);
435        result
436    }
437
438    /// Run one isolated dispatch and retain its full context for agent hooks.
439    pub(crate) async fn dispatch(
440        &self,
441        tool_name: &str,
442        args: &str,
443        context: &ToolContext,
444    ) -> ToolDispatch {
445        #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
446        let tool = {
447            let mut state = self.0.write().await;
448            state.retire_disconnected_tools();
449            state.toolset.get(tool_name).cloned()
450        };
451        #[cfg(not(all(feature = "rmcp", not(target_family = "wasm"))))]
452        let tool = {
453            let state = self.0.read().await;
454            state.toolset.get(tool_name).cloned()
455        };
456        dispatch_tool(tool_name, args.to_string(), tool, context).await
457    }
458
459    /// Retrieve tool definitions, optionally using a prompt to select
460    /// dynamic tools from configured vector stores.
461    pub async fn get_tool_defs(
462        &self,
463        prompt: Option<String>,
464    ) -> Result<Vec<ToolDefinition>, ToolServerError> {
465        Ok(self.snapshot_tool_defs(prompt).await?.definitions.clone())
466    }
467
468    /// Resolve one ordered provider/dispatch snapshot for an agent turn.
469    ///
470    /// Retrieval runs without holding the registry lock. Once the selected IDs
471    /// are known, one read lock resolves every dynamic and always-exposed name
472    /// to an exact implementation. That single instant is the turn boundary:
473    /// later replacements are visible only to the next snapshot.
474    pub(crate) async fn snapshot_tool_defs(
475        &self,
476        prompt: Option<String>,
477    ) -> Result<ToolRegistrySnapshot, ToolServerError> {
478        let retrieval_indexes = {
479            let state = self.0.read().await;
480            state.retrieval_indexes.clone()
481        };
482
483        let dynamic_tool_ids = if let Some(ref text) = prompt {
484            // Create a future for each dynamic tool index
485            let search_futures = retrieval_indexes.iter().map(|(num_sample, index)| {
486                let text = text.clone();
487                let num_sample = *num_sample;
488                let index = index.clone();
489
490                async move {
491                    let req = VectorSearchRequest::builder()
492                        .query(text)
493                        .samples(num_sample as u64)
494                        .build();
495
496                    let ids = index
497                        .as_ref()
498                        .top_n_ids(req.map_filter(Filter::interpret))
499                        .await?
500                        .into_iter()
501                        .map(|(_, id)| id)
502                        .collect::<Vec<String>>();
503
504                    Ok::<_, VectorStoreError>(ids)
505                }
506            });
507
508            // Execute searches concurrently and collect/flatten the IDs
509            futures::future::try_join_all(search_futures)
510                .await
511                .map_err(|e| {
512                    ToolServerError::DefinitionError(CompletionError::RequestError(Box::new(e)))
513                })?
514                .into_iter()
515                .flatten()
516                .collect::<Vec<String>>()
517        } else {
518            Vec::new()
519        };
520
521        #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
522        let tools = {
523            let mut state = self.0.write().await;
524            state.retire_disconnected_tools();
525            snapshot_registered_tools(&state, dynamic_tool_ids)
526        };
527        #[cfg(not(all(feature = "rmcp", not(target_family = "wasm"))))]
528        let tools = {
529            let state = self.0.read().await;
530            snapshot_registered_tools(&state, dynamic_tool_ids)
531        };
532
533        Ok(ToolRegistrySnapshot::new(tools))
534    }
535}
536
537fn snapshot_registered_tools(
538    state: &ToolServerState,
539    dynamic_tool_ids: Vec<String>,
540) -> IndexMap<String, RegisteredTool> {
541    let mut tools = IndexMap::new();
542
543    // Retrieved tools remain first, in index/result order. Duplicate IDs and
544    // dynamic/static overlap retain the first provider declaration.
545    for name in dynamic_tool_ids {
546        if tools.contains_key(&name) {
547            tracing::debug!(
548                tool_name = %name,
549                "dropping duplicate tool definition from the request"
550            );
551            continue;
552        }
553        match state.toolset.get(&name).cloned() {
554            Some(tool) => {
555                tools.insert(name, tool);
556            }
557            None => {
558                tracing::warn!("Tool implementation not found in toolset: {name}");
559            }
560        }
561    }
562
563    for name in state.toolset.always_exposed_names() {
564        if tools.contains_key(name) {
565            tracing::debug!(
566                tool_name = %name,
567                "dropping duplicate tool definition from the request"
568            );
569            continue;
570        }
571        if let Some(tool) = state.toolset.get(name).cloned() {
572            tools.insert(name.clone(), tool);
573        }
574    }
575    tools
576}
577
578#[derive(Debug, thiserror::Error)]
579pub enum ToolServerError {
580    #[error("Failed to retrieve tool definitions: {0}")]
581    DefinitionError(CompletionError),
582}
583#[cfg(test)]
584mod tests {
585    use std::{
586        future::{Future, pending, poll_fn},
587        sync::{
588            Arc,
589            atomic::{AtomicBool, AtomicUsize, Ordering},
590        },
591        task::Poll,
592        time::Duration,
593    };
594
595    use crate::{
596        test_utils::{
597            BarrierMockToolIndex, MockAddTool, MockBarrierTool, MockControlledTool,
598            MockSubtractTool, MockToolIndex,
599        },
600        tool::{
601            Tool, ToolContext, ToolEmbedding, ToolExecutionError, ToolSet,
602            server::{ToolServer, ToolServerHandle},
603        },
604    };
605
606    async fn execute_tool(
607        handle: &ToolServerHandle,
608        name: &str,
609        args: &str,
610    ) -> Result<String, ToolExecutionError> {
611        execute_tool_with_context(handle, name, args, &mut ToolContext::new()).await
612    }
613
614    async fn execute_tool_with_context(
615        handle: &ToolServerHandle,
616        name: &str,
617        args: &str,
618        context: &mut ToolContext,
619    ) -> Result<String, ToolExecutionError> {
620        let result = handle.execute(name, args, context).await;
621        match result.error() {
622            Some(error) => Err(error.clone()),
623            None => Ok(result.output().render()),
624        }
625    }
626
627    struct NamedTool;
628
629    impl NamedTool {
630        fn new() -> Self {
631            Self
632        }
633    }
634
635    impl Tool for NamedTool {
636        const NAME: &'static str = "registered_named";
637        type Error = rig::tool::ToolExecutionError;
638        type Args = serde_json::Value;
639        type Output = String;
640
641        fn description(&self) -> String {
642            "uses its canonical name".to_string()
643        }
644
645        fn parameters(&self) -> serde_json::Value {
646            serde_json::json!({"type": "object", "properties": {}})
647        }
648
649        async fn call(
650            &self,
651            _context: &mut crate::tool::ToolContext,
652            _args: Self::Args,
653        ) -> Result<Self::Output, crate::tool::ToolExecutionError> {
654            Ok("ok".to_string())
655        }
656    }
657
658    struct ReplacementTool {
659        description: &'static str,
660        output: &'static str,
661    }
662
663    impl Tool for ReplacementTool {
664        const NAME: &'static str = "replacement";
665        type Error = rig::tool::ToolExecutionError;
666        type Args = serde_json::Value;
667        type Output = String;
668
669        fn description(&self) -> String {
670            self.description.to_string()
671        }
672
673        fn parameters(&self) -> serde_json::Value {
674            serde_json::json!({"type": "object", "properties": {}})
675        }
676
677        async fn call(
678            &self,
679            _context: &mut ToolContext,
680            _args: Self::Args,
681        ) -> Result<Self::Output, ToolExecutionError> {
682            Ok(self.output.to_string())
683        }
684    }
685
686    #[derive(Debug, thiserror::Error)]
687    #[error("init error")]
688    struct InitError;
689
690    impl ToolEmbedding for NamedTool {
691        type InitError = InitError;
692        type Context = ();
693        type State = ();
694
695        fn embedding_docs(&self) -> Vec<String> {
696            vec!["named retrieved tool".to_string()]
697        }
698
699        fn context(&self) -> Self::Context {}
700
701        fn init(_state: Self::State, _context: Self::Context) -> Result<Self, Self::InitError> {
702            Ok(Self::new())
703        }
704    }
705
706    #[tokio::test]
707    pub async fn test_toolserver() {
708        let server = ToolServer::new();
709
710        let handle = server.run();
711
712        handle.add_tool(MockAddTool).await;
713        let res = handle.get_tool_defs(None).await.unwrap();
714
715        assert_eq!(res.len(), 1);
716
717        let json_args_as_string =
718            serde_json::to_string(&serde_json::json!({"x": 2, "y": 5})).unwrap();
719        let res = execute_tool(&handle, "add", &json_args_as_string)
720            .await
721            .unwrap();
722        assert_eq!(res, "7");
723
724        handle.remove_tool("add").await;
725        let res = handle.get_tool_defs(None).await.unwrap();
726
727        assert_eq!(res.len(), 0);
728    }
729
730    #[tokio::test]
731    async fn definition_snapshot_pins_the_exact_tool_registration() {
732        let handle = ToolServer::new()
733            .tool(ReplacementTool {
734                description: "first schema",
735                output: "first implementation",
736            })
737            .run();
738        let snapshot = handle.snapshot_tool_defs(None).await.unwrap();
739
740        handle
741            .add_tool(ReplacementTool {
742                description: "second schema",
743                output: "second implementation",
744            })
745            .await;
746
747        assert_eq!(snapshot.definitions()[0].description, "first schema");
748        let dispatch = snapshot
749            .dispatch(ReplacementTool::NAME, "{}", &ToolContext::new())
750            .await;
751        assert_eq!(dispatch.result.output().render(), "first implementation");
752
753        let live = handle
754            .dispatch(ReplacementTool::NAME, "{}", &ToolContext::new())
755            .await;
756        assert_eq!(live.result.output().render(), "second implementation");
757
758        let next_snapshot = handle.snapshot_tool_defs(None).await.unwrap();
759        assert_eq!(next_snapshot.definitions()[0].description, "second schema");
760        let dispatch = next_snapshot
761            .dispatch(ReplacementTool::NAME, "{}", &ToolContext::new())
762            .await;
763        assert_eq!(dispatch.result.output().render(), "second implementation");
764    }
765
766    #[tokio::test]
767    pub async fn test_toolserver_append_toolset_matches_add_tool() {
768        let mut via_add_tool = {
769            let handle = ToolServer::new().run();
770            handle.add_tool(MockAddTool).await;
771            handle.add_tool(MockSubtractTool).await;
772            handle.get_tool_defs(None).await.unwrap()
773        };
774        via_add_tool.sort_by(|a, b| a.name.cmp(&b.name));
775
776        let mut via_append_toolset = {
777            let handle = ToolServer::new().run();
778            let mut toolset = ToolSet::default();
779            toolset.add_tool(MockAddTool);
780            toolset.add_tool(MockSubtractTool);
781            handle.append_toolset(toolset).await;
782            handle.get_tool_defs(None).await.unwrap()
783        };
784        via_append_toolset.sort_by(|a, b| a.name.cmp(&b.name));
785
786        assert_eq!(via_add_tool.len(), via_append_toolset.len());
787        assert!(
788            via_add_tool
789                .iter()
790                .zip(via_append_toolset.iter())
791                .all(|(a, b)| a.name == b.name),
792            "append_toolset must surface the same LLM-visible tools as add_tool",
793        );
794    }
795
796    #[tokio::test]
797    pub async fn builder_tool_uses_canonical_static_name() {
798        let handle = ToolServer::new().tool(NamedTool::new()).run();
799
800        let defs = handle.get_tool_defs(None).await.unwrap();
801        assert_eq!(defs.len(), 1);
802        assert_eq!(defs[0].name, NamedTool::NAME);
803    }
804
805    #[tokio::test]
806    pub async fn handle_add_tool_uses_canonical_static_name() {
807        let handle = ToolServer::new().run();
808        handle.add_tool(NamedTool::new()).await;
809
810        let defs = handle.get_tool_defs(None).await.unwrap();
811        assert_eq!(defs.len(), 1);
812        assert_eq!(defs[0].name, NamedTool::NAME);
813    }
814
815    #[tokio::test]
816    pub async fn retrieval_resolves_canonical_key() {
817        let toolset = ToolSet::builder().retrieved_tool(NamedTool::new()).build();
818        let handle = ToolServer::new()
819            .retrieved_tools(1, MockToolIndex::new([NamedTool::NAME]), toolset)
820            .run();
821
822        let defs = handle
823            .get_tool_defs(Some("use the changing tool".to_string()))
824            .await
825            .unwrap();
826        assert_eq!(defs.len(), 1);
827        assert_eq!(defs[0].name, NamedTool::NAME);
828    }
829
830    #[tokio::test]
831    pub async fn get_tool_defs_preserves_static_registration_order() {
832        let handle = ToolServer::new().run();
833        handle.add_tool(MockSubtractTool).await;
834        handle.add_tool(MockAddTool).await;
835
836        let defs = handle.get_tool_defs(None).await.unwrap();
837        assert_eq!(
838            defs.iter().map(|def| def.name.as_str()).collect::<Vec<_>>(),
839            vec!["subtract", "add"]
840        );
841    }
842
843    #[tokio::test]
844    pub async fn get_tool_defs_dedupes_dynamic_and_static_overlap() {
845        // One shared toolset backs both lists, so a dynamically retrieved
846        // name that is also static must yield a single definition.
847        let handle = ToolServer::new()
848            .tool(MockAddTool)
849            .retrieved_tools(1, MockToolIndex::new(["add"]), ToolSet::default())
850            .run();
851
852        let defs = handle
853            .get_tool_defs(Some("add two numbers".to_string()))
854            .await
855            .unwrap();
856        assert_eq!(
857            defs.len(),
858            1,
859            "dynamic/static name overlap must not produce duplicate declarations: {:?}",
860            defs.iter().map(|def| def.name.as_str()).collect::<Vec<_>>()
861        );
862        assert_eq!(defs[0].name, "add");
863    }
864
865    #[tokio::test]
866    async fn retrieval_registration_preserves_existing_always_exposure() {
867        let handle = ToolServer::new()
868            .tool(MockAddTool)
869            .retrieved_tools(
870                1,
871                MockToolIndex::new(["add"]),
872                ToolSet::from_tools(vec![MockAddTool]),
873            )
874            .run();
875
876        let defs = handle.get_tool_defs(None).await.unwrap();
877        assert_eq!(
878            defs.iter()
879                .map(|definition| definition.name.as_str())
880                .collect::<Vec<_>>(),
881            vec!["add"],
882            "merging a retrieval implementation must not demote an always-exposed registration"
883        );
884    }
885
886    #[tokio::test]
887    pub async fn duplicate_registration_advertises_one_definition() {
888        let handle = ToolServer::new().tool(MockAddTool).run();
889        handle.add_tool(MockAddTool).await;
890
891        let mut toolset = ToolSet::default();
892        toolset.add_tool(MockAddTool);
893        handle.append_toolset(toolset).await;
894
895        let defs = handle.get_tool_defs(None).await.unwrap();
896        assert_eq!(
897            defs.len(),
898            1,
899            "re-registering a name must not advertise duplicate declarations"
900        );
901        assert_eq!(defs[0].name, "add");
902    }
903
904    #[tokio::test]
905    pub async fn test_toolserver_retrieved_tools() {
906        // Create a toolset with both tools
907        let mut toolset = ToolSet::default();
908        toolset.add_tool(MockAddTool);
909        toolset.add_tool(MockSubtractTool);
910
911        // Create a mock index that will return "subtract" as the dynamic tool
912        let mock_index = MockToolIndex::new(["subtract"]);
913
914        // Build server with static tool "add" and dynamic tools from the mock index
915        let server = ToolServer::new().tool(MockAddTool).retrieved_tools(
916            1,
917            mock_index,
918            ToolSet::from_tools(vec![MockSubtractTool]),
919        );
920
921        let handle = server.run();
922
923        // Test with None prompt - should only return static tools
924        let res = handle.get_tool_defs(None).await.unwrap();
925        assert_eq!(res.len(), 1);
926        assert_eq!(res[0].name, "add");
927
928        // Test with Some prompt - should return both static and dynamic tools
929        let res = handle
930            .get_tool_defs(Some("calculate difference".to_string()))
931            .await
932            .unwrap();
933        assert_eq!(res.len(), 2);
934
935        // Check that both tools are present (order may vary)
936        let tool_names: Vec<&str> = res.iter().map(|t| t.name.as_str()).collect();
937        assert!(tool_names.contains(&"add"));
938        assert!(tool_names.contains(&"subtract"));
939    }
940
941    #[tokio::test]
942    pub async fn test_toolserver_retrieved_tools_missing_implementation() {
943        // Create a mock index that returns a tool ID that doesn't exist in the toolset
944        let mock_index = MockToolIndex::new(["nonexistent_tool"]);
945
946        // Build server with only static tool, but dynamic index references missing tool
947        let server =
948            ToolServer::new()
949                .tool(MockAddTool)
950                .retrieved_tools(1, mock_index, ToolSet::default());
951
952        let handle = server.run();
953
954        // Test with Some prompt - should only return static tool since dynamic tool is missing
955        let res = handle
956            .get_tool_defs(Some("some query".to_string()))
957            .await
958            .unwrap();
959        assert_eq!(res.len(), 1);
960        assert_eq!(res[0].name, "add");
961    }
962
963    #[tokio::test]
964    pub async fn test_toolserver_concurrent_tool_execution() {
965        let num_calls = 3;
966        let barrier = Arc::new(tokio::sync::Barrier::new(num_calls));
967
968        let server = ToolServer::new().tool(MockBarrierTool::new(barrier.clone()));
969        let handle = server.run();
970
971        // Make concurrent calls
972        let futures: Vec<_> = (0..num_calls)
973            .map(|_| execute_tool(&handle, "barrier_tool", "{}"))
974            .collect();
975
976        // If execution is sequential, the first call will block at the barrier forever.
977        // We use a 1-second timeout to fail fast instead of hanging the test runner.
978        let result =
979            tokio::time::timeout(Duration::from_secs(1), futures::future::join_all(futures)).await;
980
981        assert!(
982            result.is_ok(),
983            "Tool execution deadlocked! Tools are executing sequentially instead of concurrently."
984        );
985
986        // All calls should succeed
987        for res in result.unwrap() {
988            assert!(res.is_ok(), "Tool call failed: {:?}", res);
989            assert_eq!(res.unwrap(), "done");
990        }
991    }
992
993    #[tokio::test]
994    pub async fn test_toolserver_write_while_tool_running() {
995        let started = Arc::new(tokio::sync::Notify::new());
996        let allow_finish = Arc::new(tokio::sync::Notify::new());
997
998        // Build server with the controlled tool that waits at a barrier during execution
999        let tool = MockControlledTool::new(started.clone(), allow_finish.clone());
1000
1001        let server = ToolServer::new().tool(tool);
1002        let handle = server.run();
1003
1004        // Start tool call in background
1005        let handle_clone = handle.clone();
1006        let call_task =
1007            tokio::spawn(async move { execute_tool(&handle_clone, "controlled", "{}").await });
1008
1009        // Wait until we are strictly inside `call()`
1010        started.notified().await;
1011
1012        // Try to write to the state (add a tool) while the tool call is mid-execution.
1013        // If the read lock is incorrectly held across tool execution, this will deadlock.
1014        let add_result =
1015            tokio::time::timeout(Duration::from_secs(1), handle.add_tool(MockAddTool)).await;
1016
1017        assert!(
1018            add_result.is_ok(),
1019            "Writing to ToolServer deadlocked! The read lock is being held across tool execution."
1020        );
1021
1022        // Allow the background tool to finish and clean up
1023        allow_finish.notify_one();
1024        let call_result = call_task.await.unwrap();
1025        assert_eq!(call_result.unwrap(), "42");
1026    }
1027
1028    #[tokio::test]
1029    pub async fn test_toolserver_parallel_retrieval() {
1030        // We expect exactly 2 parallel searches to hit the barrier at the same time
1031        let barrier = Arc::new(tokio::sync::Barrier::new(2));
1032
1033        let index1 = BarrierMockToolIndex::new(barrier.clone(), "add");
1034        let index2 = BarrierMockToolIndex::new(barrier.clone(), "subtract");
1035
1036        // Put both tools in the toolset so they resolve correctly
1037        let mut toolset = ToolSet::default();
1038        toolset.add_tool(MockAddTool);
1039        toolset.add_tool(MockSubtractTool);
1040
1041        let server = ToolServer::new()
1042            .retrieved_tools(1, index1, ToolSet::default())
1043            .retrieved_tools(1, index2, toolset);
1044
1045        let handle = server.run();
1046
1047        // This will trigger a search across both indices.
1048        // If fetched sequentially, the first index will wait at the barrier forever.
1049        let get_defs = tokio::time::timeout(
1050            std::time::Duration::from_secs(1),
1051            handle.get_tool_defs(Some("do math".to_string())),
1052        )
1053        .await;
1054
1055        assert!(
1056            get_defs.is_ok(),
1057            "Dynamic tools were fetched sequentially! The first query deadlocked waiting for the second query to start."
1058        );
1059
1060        let defs = get_defs.unwrap().unwrap();
1061        assert_eq!(defs.len(), 2);
1062
1063        let tool_names: Vec<&str> = defs.iter().map(|t| t.name.as_str()).collect();
1064        assert!(tool_names.contains(&"add"));
1065        assert!(tool_names.contains(&"subtract"));
1066    }
1067
1068    #[derive(Clone)]
1069    struct SessionId(String);
1070
1071    struct CloneTrackedContext {
1072        clones: Arc<AtomicUsize>,
1073        value: usize,
1074    }
1075
1076    impl Clone for CloneTrackedContext {
1077        fn clone(&self) -> Self {
1078            self.clones.fetch_add(1, Ordering::SeqCst);
1079            Self {
1080                clones: self.clones.clone(),
1081                value: self.value,
1082            }
1083        }
1084    }
1085
1086    #[derive(serde::Deserialize, serde::Serialize)]
1087    struct ContextReader;
1088
1089    impl crate::tool::Tool for ContextReader {
1090        const NAME: &'static str = "context_reader";
1091        type Error = rig::tool::ToolExecutionError;
1092        type Args = serde_json::Value;
1093        type Output = String;
1094
1095        fn description(&self) -> String {
1096            "Reads SessionId from context".to_string()
1097        }
1098
1099        fn parameters(&self) -> serde_json::Value {
1100            serde_json::json!({"type": "object", "properties": {}})
1101        }
1102
1103        async fn call(
1104            &self,
1105            context: &mut ToolContext,
1106            _args: Self::Args,
1107        ) -> Result<Self::Output, ToolExecutionError> {
1108            if let Some(value) = context.get_mut::<CloneTrackedContext>() {
1109                value.value += 1;
1110                let result_value = value.value;
1111                context.insert_result(result_value);
1112            }
1113            Ok(context
1114                .get::<SessionId>()
1115                .map(|session| format!("session:{}", session.0))
1116                .unwrap_or_else(|| "no session".to_string()))
1117        }
1118    }
1119
1120    #[tokio::test]
1121    async fn context_reaches_the_single_execute_path() {
1122        let handle = ToolServer::new().tool(ContextReader).run();
1123        let mut context = ToolContext::new();
1124        context.insert(SessionId("abc-123".to_string()));
1125        let result = execute_tool_with_context(&handle, "context_reader", "{}", &mut context)
1126            .await
1127            .unwrap();
1128        assert_eq!(result, "session:abc-123");
1129    }
1130
1131    #[tokio::test]
1132    async fn server_dispatch_snapshot_clones_once_and_only_publishes_result_metadata() {
1133        let handle = ToolServer::new().tool(ContextReader).run();
1134        let clones = Arc::new(AtomicUsize::new(0));
1135        let mut context = ToolContext::new();
1136        context.insert(CloneTrackedContext {
1137            clones: clones.clone(),
1138            value: 0,
1139        });
1140
1141        let result = execute_tool_with_context(&handle, "context_reader", "{}", &mut context)
1142            .await
1143            .unwrap();
1144
1145        assert_eq!(result, "no session");
1146        assert_eq!(clones.load(Ordering::SeqCst), 1);
1147        assert_eq!(
1148            context
1149                .get::<CloneTrackedContext>()
1150                .map(|value| value.value),
1151            Some(0),
1152            "tool-local inbound mutations must not change the caller's context"
1153        );
1154        assert_eq!(context.result::<usize>(), Some(&1));
1155    }
1156
1157    struct PendingTool(Arc<AtomicBool>);
1158
1159    impl Tool for PendingTool {
1160        const NAME: &'static str = "pending";
1161        type Error = rig::tool::ToolExecutionError;
1162        type Args = ();
1163        type Output = ();
1164
1165        fn description(&self) -> String {
1166            "never completes".into()
1167        }
1168
1169        fn parameters(&self) -> serde_json::Value {
1170            serde_json::json!({"type": "object"})
1171        }
1172
1173        async fn call(
1174            &self,
1175            context: &mut ToolContext,
1176            _args: Self::Args,
1177        ) -> Result<Self::Output, ToolExecutionError> {
1178            context.insert_result("unpublished".to_string());
1179            self.0.store(true, Ordering::SeqCst);
1180            pending().await
1181        }
1182    }
1183
1184    #[tokio::test]
1185    async fn cancelled_server_dispatch_does_not_retain_stale_result_metadata() {
1186        let started = Arc::new(AtomicBool::new(false));
1187        let handle = ToolServer::new().tool(PendingTool(started.clone())).run();
1188        let mut context = ToolContext::new();
1189        context.insert_result("stale".to_string());
1190
1191        let mut execution = Box::pin(handle.execute(PendingTool::NAME, "null", &mut context));
1192        tokio::time::timeout(
1193            Duration::from_secs(1),
1194            poll_fn(|cx| {
1195                assert!(execution.as_mut().poll(cx).is_pending());
1196                started.load(Ordering::SeqCst).then_some(()).map_or_else(
1197                    || {
1198                        cx.waker().wake_by_ref();
1199                        Poll::Pending
1200                    },
1201                    Poll::Ready,
1202                )
1203            }),
1204        )
1205        .await
1206        .expect("pending tool did not start");
1207        drop(execution);
1208
1209        assert!(context.result::<String>().is_none());
1210    }
1211
1212    #[tokio::test]
1213    async fn empty_tool_context_uses_default() {
1214        let handle = ToolServer::new().tool(ContextReader).run();
1215        let result = execute_tool(&handle, "context_reader", "{}").await.unwrap();
1216
1217        assert_eq!(result, "no session");
1218    }
1219
1220    #[tokio::test]
1221    async fn tool_ignoring_context_still_works() {
1222        let handle = ToolServer::new().tool(MockAddTool).run();
1223        let mut context = ToolContext::new();
1224        context.insert(SessionId("ignored".to_string()));
1225        let args = serde_json::to_string(&serde_json::json!({"x": 3, "y": 7})).unwrap();
1226        let result = execute_tool_with_context(&handle, "add", &args, &mut context)
1227            .await
1228            .unwrap();
1229
1230        assert_eq!(result, "10");
1231    }
1232
1233    #[tokio::test]
1234    async fn execute_classifies_a_missing_tool_as_not_found() {
1235        let handle = ToolServer::new().tool(MockAddTool).run();
1236        let error = execute_tool(&handle, "does_not_exist", "{}")
1237            .await
1238            .unwrap_err();
1239        assert_eq!(error.kind(), crate::tool::ToolErrorKind::NotFound);
1240        assert!(
1241            error
1242                .model_feedback()
1243                .is_some_and(|feedback| feedback.contains("does_not_exist"))
1244        );
1245    }
1246}