Skip to main content

rig_core/tool/
server.rs

1use std::sync::Arc;
2
3use tokio::sync::RwLock;
4
5use crate::{
6    completion::{CompletionError, ToolDefinition},
7    tool::{
8        Tool, ToolCallExtensions, ToolDyn, ToolExecutionResult, ToolFailure, ToolSet, ToolSetError,
9    },
10    vector_store::{VectorSearchRequest, VectorStoreError, VectorStoreIndexDyn, request::Filter},
11};
12
13/// Append `name` to the advertised static-tool list unless already present.
14/// Registration is last-wins on the toolset, so the name list only needs
15/// first-occurrence order: a re-registered name keeps its original position
16/// while the toolset swaps in the new implementation. Providers reject
17/// duplicate function declarations, so the list must stay unique.
18fn push_unique_name(names: &mut Vec<String>, name: String) {
19    if !names.contains(&name) {
20        names.push(name);
21    }
22}
23
24/// Shared state behind a `ToolServerHandle`.
25struct ToolServerState {
26    /// Static tool names that persist until explicitly removed.
27    static_tool_names: Vec<String>,
28    /// Dynamic tools fetched from vector stores on each prompt.
29    dynamic_tools: Vec<(usize, Arc<dyn VectorStoreIndexDyn + Send + Sync>)>,
30    /// The toolset where tools are registered and executed.
31    toolset: ToolSet,
32}
33
34/// Builder for constructing a [`ToolServerHandle`].
35///
36/// Accumulates tools and configuration, then produces a shared handle via
37/// [`run()`](ToolServer::run).
38pub struct ToolServer {
39    static_tool_names: Vec<String>,
40    dynamic_tools: Vec<(usize, Arc<dyn VectorStoreIndexDyn + Send + Sync>)>,
41    toolset: ToolSet,
42}
43
44impl Default for ToolServer {
45    fn default() -> Self {
46        Self::new()
47    }
48}
49
50impl ToolServer {
51    pub fn new() -> Self {
52        Self {
53            static_tool_names: Vec::new(),
54            dynamic_tools: Vec::new(),
55            toolset: ToolSet::default(),
56        }
57    }
58
59    pub(crate) fn static_tool_names(mut self, names: Vec<String>) -> Self {
60        // Last-wins registration replaces the implementation but keeps the
61        // original position, so the advertised list dedupes to first
62        // occurrence (duplicate declarations are rejected by providers).
63        self.static_tool_names = Vec::with_capacity(names.len());
64        for name in names {
65            push_unique_name(&mut self.static_tool_names, name);
66        }
67        self
68    }
69
70    pub(crate) fn add_tools(mut self, tools: ToolSet) -> Self {
71        self.toolset = tools;
72        self
73    }
74
75    pub(crate) fn add_dynamic_tools(
76        mut self,
77        dyn_tools: Vec<(usize, Arc<dyn VectorStoreIndexDyn + Send + Sync>)>,
78    ) -> Self {
79        self.dynamic_tools = dyn_tools;
80        self
81    }
82
83    /// Add a static tool to the agent. Re-registering an existing name
84    /// replaces the implementation (last wins) and keeps its position.
85    pub fn tool(mut self, tool: impl Tool + 'static) -> Self {
86        let toolname = self.toolset.add_tool(tool);
87        push_unique_name(&mut self.static_tool_names, toolname);
88        self
89    }
90
91    /// Add an MCP tool (from `rmcp`) to the agent, bounded by
92    /// [`DEFAULT_MCP_TOOL_TIMEOUT`](crate::tool::rmcp::DEFAULT_MCP_TOOL_TIMEOUT)
93    /// (see issue #1914). Use [`rmcp_tool_with_timeout`](Self::rmcp_tool_with_timeout)
94    /// to change or disable it.
95    #[cfg_attr(docsrs, doc(cfg(feature = "rmcp")))]
96    #[cfg(feature = "rmcp")]
97    pub fn rmcp_tool(self, tool: rmcp::model::Tool, client: rmcp::service::ServerSink) -> Self {
98        self.rmcp_tool_with_timeout(tool, client, crate::tool::rmcp::DEFAULT_MCP_TOOL_TIMEOUT)
99    }
100
101    /// Add an MCP tool (from `rmcp`) with a per-call timeout (see issue #1914).
102    ///
103    /// Pass a [`Duration`](std::time::Duration) to bound the call, or `None` to
104    /// disable the timeout (unbounded).
105    #[cfg_attr(docsrs, doc(cfg(feature = "rmcp")))]
106    #[cfg(feature = "rmcp")]
107    pub fn rmcp_tool_with_timeout(
108        mut self,
109        tool: rmcp::model::Tool,
110        client: rmcp::service::ServerSink,
111        timeout: impl Into<Option<std::time::Duration>>,
112    ) -> Self {
113        use crate::tool::rmcp::McpTool;
114        let toolname = self
115            .toolset
116            .add_tool(McpTool::from_mcp_server(tool, client).with_timeout(timeout));
117        push_unique_name(&mut self.static_tool_names, toolname);
118        self
119    }
120
121    /// Add some dynamic tools to the agent. On each prompt, `sample` tools from the
122    /// dynamic toolset will be inserted in the request.
123    pub fn dynamic_tools(
124        mut self,
125        sample: usize,
126        dynamic_tools: impl VectorStoreIndexDyn + Send + Sync + 'static,
127        toolset: ToolSet,
128    ) -> Self {
129        self.dynamic_tools.push((sample, Arc::new(dynamic_tools)));
130        self.toolset.add_tools(toolset);
131        self
132    }
133
134    /// Consume the builder and return a shared [`ToolServerHandle`].
135    pub fn run(self) -> ToolServerHandle {
136        ToolServerHandle(Arc::new(RwLock::new(ToolServerState {
137            static_tool_names: self.static_tool_names,
138            dynamic_tools: self.dynamic_tools,
139            toolset: self.toolset,
140        })))
141    }
142}
143
144/// A cheaply-cloneable handle to the shared tool server state.
145///
146/// All operations acquire locks directly on the underlying state.
147/// Multiple handles (e.g. across agents) can share the same state
148/// without channel-based message routing.
149#[derive(Clone)]
150pub struct ToolServerHandle(Arc<RwLock<ToolServerState>>);
151
152impl ToolServerHandle {
153    /// Register a new static tool. Re-registering an existing name replaces
154    /// the implementation (last wins) and keeps its position.
155    pub async fn add_tool(&self, tool: impl ToolDyn + 'static) -> Result<(), ToolServerError> {
156        let mut state = self.0.write().await;
157        let toolname = state.toolset.add_tool_boxed(Box::new(tool));
158        push_unique_name(&mut state.static_tool_names, toolname);
159        Ok(())
160    }
161
162    /// Merge an entire toolset into the server. Tool names from `toolset`
163    /// are appended to the static-tool list in `toolset`'s registration
164    /// order, so the tools become visible to the LLM via
165    /// [`Self::get_tool_defs`]. Existing names are replaced (last wins) and
166    /// keep their position.
167    pub async fn append_toolset(&self, toolset: ToolSet) -> Result<(), ToolServerError> {
168        let mut state = self.0.write().await;
169        for name in toolset.ordered_names() {
170            push_unique_name(&mut state.static_tool_names, name.clone());
171        }
172        state.toolset.add_tools(toolset);
173        Ok(())
174    }
175
176    /// Remove a tool by name from both the toolset and the static list.
177    pub async fn remove_tool(&self, tool_name: &str) -> Result<(), ToolServerError> {
178        let mut state = self.0.write().await;
179        state.static_tool_names.retain(|x| *x != tool_name);
180        state.toolset.delete_tool(tool_name);
181        Ok(())
182    }
183
184    /// Look up and execute a tool by name.
185    ///
186    /// The tool handle is cloned under a brief read lock so that
187    /// long-running tool executions never block writers.
188    pub async fn call_tool(&self, tool_name: &str, args: &str) -> Result<String, ToolServerError> {
189        self.call_tool_with_extensions(tool_name, args, &ToolCallExtensions::EMPTY)
190            .await
191    }
192
193    /// Look up and execute a tool by name with per-call runtime extensions.
194    ///
195    /// The extensions are threaded through to [`Tool::call_with_extensions`],
196    /// allowing tools to access caller-provided values (auth tokens, session
197    /// IDs, etc.). The tool handle is cloned under a brief read lock so that
198    /// long-running tool executions never block writers.
199    pub async fn call_tool_with_extensions(
200        &self,
201        tool_name: &str,
202        args: &str,
203        extensions: &ToolCallExtensions,
204    ) -> Result<String, ToolServerError> {
205        let tool = {
206            let state = self.0.read().await;
207            state.toolset.get(tool_name).cloned()
208        };
209
210        match tool {
211            Some(tool) => {
212                tracing::debug!(target: "rig",
213                    "Calling tool {tool_name} with args:\n{}",
214                    serde_json::to_string_pretty(&args).unwrap_or_default()
215                );
216                tool.call_with_extensions(args.to_string(), extensions)
217                    .await
218                    .map_err(|e| ToolSetError::ToolCallError(e).into())
219            }
220            None => Err(ToolServerError::ToolsetError(
221                ToolSetError::ToolNotFoundError(tool_name.to_string()),
222            )),
223        }
224    }
225
226    /// Look up and execute a tool by name, returning the structured
227    /// [`ToolExecutionResult`] (model output + [`ToolOutcome`](crate::tool::ToolOutcome)
228    /// + result extensions).
229    ///
230    /// The structured counterpart of [`call_tool_with_extensions`](Self::call_tool_with_extensions),
231    /// and the path the agent loop drives so hooks, tracing, and policies observe
232    /// the structured outcome. A missing tool resolves to a
233    /// [`NotFound`](crate::tool::ToolFailureKind::NotFound) outcome rather than a
234    /// `Result::Err`. The tool handle is cloned under a brief read lock so that
235    /// long-running tool executions never block writers.
236    pub async fn call_tool_structured(
237        &self,
238        tool_name: &str,
239        args: &str,
240        extensions: &ToolCallExtensions,
241    ) -> ToolExecutionResult {
242        let tool = {
243            let state = self.0.read().await;
244            state.toolset.get(tool_name).cloned()
245        };
246
247        match tool {
248            Some(tool) => {
249                tracing::debug!(target: "rig",
250                    "Calling tool {tool_name} with args:\n{}",
251                    serde_json::to_string_pretty(&args).unwrap_or_default()
252                );
253                tool.call_structured(args.to_string(), extensions).await
254            }
255            None => ToolExecutionResult::failed(
256                format!("tool `{tool_name}` not found"),
257                ToolFailure::not_found(format!("no tool named `{tool_name}` is registered")),
258            ),
259        }
260    }
261
262    /// Retrieve tool definitions, optionally using a prompt to select
263    /// dynamic tools from configured vector stores.
264    pub async fn get_tool_defs(
265        &self,
266        prompt: Option<String>,
267    ) -> Result<Vec<ToolDefinition>, ToolServerError> {
268        // Snapshot the metadata we need under a brief read lock
269        let (static_tool_names, dynamic_tools) = {
270            let state = self.0.read().await;
271            (state.static_tool_names.clone(), state.dynamic_tools.clone())
272        };
273
274        let mut tools = if let Some(ref text) = prompt {
275            // Create a future for each dynamic tool index
276            let search_futures = dynamic_tools.iter().map(|(num_sample, index)| {
277                let text = text.clone();
278                let num_sample = *num_sample;
279                let index = index.clone();
280
281                async move {
282                    let req = VectorSearchRequest::builder()
283                        .query(text)
284                        .samples(num_sample as u64)
285                        .build();
286
287                    let ids = index
288                        .as_ref()
289                        .top_n_ids(req.map_filter(Filter::interpret))
290                        .await?
291                        .into_iter()
292                        .map(|(_, id)| id)
293                        .collect::<Vec<String>>();
294
295                    Ok::<_, VectorStoreError>(ids)
296                }
297            });
298
299            // Execute searches concurrently and collect/flatten the IDs
300            let dynamic_tool_ids: Vec<String> = futures::future::try_join_all(search_futures)
301                .await
302                .map_err(|e| {
303                    ToolServerError::DefinitionError(CompletionError::RequestError(Box::new(e)))
304                })?
305                .into_iter()
306                .flatten()
307                .collect();
308
309            let dynamic_tool_handles: Vec<_> = {
310                let state = self.0.read().await;
311                dynamic_tool_ids
312                    .iter()
313                    .filter_map(|doc| {
314                        let handle = state.toolset.get(doc).cloned();
315                        if handle.is_none() {
316                            tracing::warn!("Tool implementation not found in toolset: {}", doc);
317                        }
318                        handle.map(|handle| (doc.clone(), handle))
319                    })
320                    .collect()
321            };
322
323            dynamic_tool_handles
324                .into_iter()
325                .map(|(name, tool)| tool.definition_with_name(name))
326                .collect()
327        } else {
328            Vec::new()
329        };
330
331        let static_tool_handles: Vec<_> = {
332            let state = self.0.read().await;
333            static_tool_names
334                .iter()
335                .filter_map(|toolname| {
336                    let handle = state.toolset.get(toolname).cloned();
337                    if handle.is_none() {
338                        tracing::warn!("Tool implementation not found in toolset: {}", toolname);
339                    }
340                    handle.map(|handle| (toolname.clone(), handle))
341                })
342                .collect()
343        };
344
345        for (name, tool) in static_tool_handles {
346            tools.push(tool.definition_with_name(name));
347        }
348
349        // One shared toolset backs both lists, so a name appearing in the
350        // dynamic AND static lists (or retrieved by two indexes) refers to
351        // the same tool. Keep the first definition and drop exact-name
352        // repeats: providers reject duplicate function declarations.
353        let mut seen = std::collections::HashSet::new();
354        tools.retain(|def| {
355            let fresh = seen.insert(def.name.clone());
356            if !fresh {
357                tracing::debug!(
358                    tool_name = %def.name,
359                    "dropping duplicate tool definition from the request"
360                );
361            }
362            fresh
363        });
364
365        Ok(tools)
366    }
367}
368
369#[derive(Debug, thiserror::Error)]
370pub enum ToolServerError {
371    #[error("Toolset error: {0}")]
372    ToolsetError(#[from] ToolSetError),
373    #[error("Failed to retrieve tool definitions: {0}")]
374    DefinitionError(CompletionError),
375}
376
377#[cfg(test)]
378mod tests {
379    use std::{
380        sync::{
381            Arc,
382            atomic::{AtomicUsize, Ordering},
383        },
384        time::Duration,
385    };
386
387    use crate::{
388        test_utils::{
389            BarrierMockToolIndex, MockAddTool, MockBarrierTool, MockControlledTool,
390            MockSubtractTool, MockToolError, MockToolIndex,
391        },
392        tool::{Tool, ToolEmbedding, ToolSet, server::ToolServer},
393    };
394
395    struct ChangingNameTool {
396        calls: AtomicUsize,
397    }
398
399    impl ChangingNameTool {
400        fn new() -> Self {
401            Self {
402                calls: AtomicUsize::new(0),
403            }
404        }
405    }
406
407    impl Tool for ChangingNameTool {
408        const NAME: &'static str = "unused";
409        type Error = MockToolError;
410        type Args = serde_json::Value;
411        type Output = String;
412
413        fn name(&self) -> String {
414            match self.calls.fetch_add(1, Ordering::SeqCst) {
415                0 => "registered_changing".to_string(),
416                _ => "changed_after_registration".to_string(),
417            }
418        }
419
420        fn description(&self) -> String {
421            "changes name after registration".to_string()
422        }
423
424        fn parameters(&self) -> serde_json::Value {
425            serde_json::json!({"type": "object", "properties": {}})
426        }
427
428        async fn call(&self, _args: Self::Args) -> Result<Self::Output, Self::Error> {
429            Ok("ok".to_string())
430        }
431    }
432
433    #[derive(Debug, thiserror::Error)]
434    #[error("init error")]
435    struct InitError;
436
437    impl ToolEmbedding for ChangingNameTool {
438        type InitError = InitError;
439        type Context = ();
440        type State = ();
441
442        fn embedding_docs(&self) -> Vec<String> {
443            vec!["changing dynamic tool".to_string()]
444        }
445
446        fn context(&self) -> Self::Context {}
447
448        fn init(_state: Self::State, _context: Self::Context) -> Result<Self, Self::InitError> {
449            Ok(Self::new())
450        }
451    }
452
453    #[tokio::test]
454    pub async fn test_toolserver() {
455        let server = ToolServer::new();
456
457        let handle = server.run();
458
459        handle.add_tool(MockAddTool).await.unwrap();
460        let res = handle.get_tool_defs(None).await.unwrap();
461
462        assert_eq!(res.len(), 1);
463
464        let json_args_as_string =
465            serde_json::to_string(&serde_json::json!({"x": 2, "y": 5})).unwrap();
466        let res = handle.call_tool("add", &json_args_as_string).await.unwrap();
467        assert_eq!(res, "7");
468
469        handle.remove_tool("add").await.unwrap();
470        let res = handle.get_tool_defs(None).await.unwrap();
471
472        assert_eq!(res.len(), 0);
473    }
474
475    #[tokio::test]
476    pub async fn test_toolserver_append_toolset_matches_add_tool() {
477        let mut via_add_tool = {
478            let handle = ToolServer::new().run();
479            handle.add_tool(MockAddTool).await.unwrap();
480            handle.add_tool(MockSubtractTool).await.unwrap();
481            handle.get_tool_defs(None).await.unwrap()
482        };
483        via_add_tool.sort_by(|a, b| a.name.cmp(&b.name));
484
485        let mut via_append_toolset = {
486            let handle = ToolServer::new().run();
487            let mut toolset = ToolSet::default();
488            toolset.add_tool(MockAddTool);
489            toolset.add_tool(MockSubtractTool);
490            handle.append_toolset(toolset).await.unwrap();
491            handle.get_tool_defs(None).await.unwrap()
492        };
493        via_append_toolset.sort_by(|a, b| a.name.cmp(&b.name));
494
495        assert_eq!(via_add_tool.len(), via_append_toolset.len());
496        assert!(
497            via_add_tool
498                .iter()
499                .zip(via_append_toolset.iter())
500                .all(|(a, b)| a.name == b.name),
501            "append_toolset must surface the same LLM-visible tools as add_tool",
502        );
503    }
504
505    #[tokio::test]
506    pub async fn builder_tool_uses_registered_key_for_static_names() {
507        let handle = ToolServer::new().tool(ChangingNameTool::new()).run();
508
509        let defs = handle.get_tool_defs(None).await.unwrap();
510        assert_eq!(defs.len(), 1);
511        assert_eq!(defs[0].name, "registered_changing");
512    }
513
514    #[tokio::test]
515    pub async fn handle_add_tool_uses_registered_key_for_static_names() {
516        let handle = ToolServer::new().run();
517        handle.add_tool(ChangingNameTool::new()).await.unwrap();
518
519        let defs = handle.get_tool_defs(None).await.unwrap();
520        assert_eq!(defs.len(), 1);
521        assert_eq!(defs[0].name, "registered_changing");
522    }
523
524    #[tokio::test]
525    pub async fn dynamic_retrieval_resolves_registered_key() {
526        let toolset = ToolSet::builder()
527            .dynamic_tool(ChangingNameTool::new())
528            .build();
529        let handle = ToolServer::new()
530            .dynamic_tools(1, MockToolIndex::new(["registered_changing"]), toolset)
531            .run();
532
533        let defs = handle
534            .get_tool_defs(Some("use the changing tool".to_string()))
535            .await
536            .unwrap();
537        assert_eq!(defs.len(), 1);
538        assert_eq!(defs[0].name, "registered_changing");
539    }
540
541    #[tokio::test]
542    pub async fn get_tool_defs_preserves_static_registration_order() {
543        let handle = ToolServer::new().run();
544        handle.add_tool(MockSubtractTool).await.unwrap();
545        handle.add_tool(MockAddTool).await.unwrap();
546
547        let defs = handle.get_tool_defs(None).await.unwrap();
548        assert_eq!(
549            defs.iter().map(|def| def.name.as_str()).collect::<Vec<_>>(),
550            vec!["subtract", "add"]
551        );
552    }
553
554    #[tokio::test]
555    pub async fn get_tool_defs_dedupes_dynamic_and_static_overlap() {
556        // One shared toolset backs both lists, so a dynamically retrieved
557        // name that is also static must yield a single definition.
558        let handle = ToolServer::new()
559            .tool(MockAddTool)
560            .dynamic_tools(1, MockToolIndex::new(["add"]), ToolSet::default())
561            .run();
562
563        let defs = handle
564            .get_tool_defs(Some("add two numbers".to_string()))
565            .await
566            .unwrap();
567        assert_eq!(
568            defs.len(),
569            1,
570            "dynamic/static name overlap must not produce duplicate declarations: {:?}",
571            defs.iter().map(|def| def.name.as_str()).collect::<Vec<_>>()
572        );
573        assert_eq!(defs[0].name, "add");
574    }
575
576    #[tokio::test]
577    pub async fn duplicate_registration_advertises_one_definition() {
578        let handle = ToolServer::new().tool(MockAddTool).run();
579        handle.add_tool(MockAddTool).await.unwrap();
580
581        let mut toolset = ToolSet::default();
582        toolset.add_tool(MockAddTool);
583        handle.append_toolset(toolset).await.unwrap();
584
585        let defs = handle.get_tool_defs(None).await.unwrap();
586        assert_eq!(
587            defs.len(),
588            1,
589            "re-registering a name must not advertise duplicate declarations"
590        );
591        assert_eq!(defs[0].name, "add");
592    }
593
594    #[tokio::test]
595    pub async fn test_toolserver_dynamic_tools() {
596        // Create a toolset with both tools
597        let mut toolset = ToolSet::default();
598        toolset.add_tool(MockAddTool);
599        toolset.add_tool(MockSubtractTool);
600
601        // Create a mock index that will return "subtract" as the dynamic tool
602        let mock_index = MockToolIndex::new(["subtract"]);
603
604        // Build server with static tool "add" and dynamic tools from the mock index
605        let server = ToolServer::new().tool(MockAddTool).dynamic_tools(
606            1,
607            mock_index,
608            ToolSet::from_tools(vec![MockSubtractTool]),
609        );
610
611        let handle = server.run();
612
613        // Test with None prompt - should only return static tools
614        let res = handle.get_tool_defs(None).await.unwrap();
615        assert_eq!(res.len(), 1);
616        assert_eq!(res[0].name, "add");
617
618        // Test with Some prompt - should return both static and dynamic tools
619        let res = handle
620            .get_tool_defs(Some("calculate difference".to_string()))
621            .await
622            .unwrap();
623        assert_eq!(res.len(), 2);
624
625        // Check that both tools are present (order may vary)
626        let tool_names: Vec<&str> = res.iter().map(|t| t.name.as_str()).collect();
627        assert!(tool_names.contains(&"add"));
628        assert!(tool_names.contains(&"subtract"));
629    }
630
631    #[tokio::test]
632    pub async fn test_toolserver_dynamic_tools_missing_implementation() {
633        // Create a mock index that returns a tool ID that doesn't exist in the toolset
634        let mock_index = MockToolIndex::new(["nonexistent_tool"]);
635
636        // Build server with only static tool, but dynamic index references missing tool
637        let server =
638            ToolServer::new()
639                .tool(MockAddTool)
640                .dynamic_tools(1, mock_index, ToolSet::default());
641
642        let handle = server.run();
643
644        // Test with Some prompt - should only return static tool since dynamic tool is missing
645        let res = handle
646            .get_tool_defs(Some("some query".to_string()))
647            .await
648            .unwrap();
649        assert_eq!(res.len(), 1);
650        assert_eq!(res[0].name, "add");
651    }
652
653    #[tokio::test]
654    pub async fn test_toolserver_concurrent_tool_execution() {
655        let num_calls = 3;
656        let barrier = Arc::new(tokio::sync::Barrier::new(num_calls));
657
658        let server = ToolServer::new().tool(MockBarrierTool::new(barrier.clone()));
659        let handle = server.run();
660
661        // Make concurrent calls
662        let futures: Vec<_> = (0..num_calls)
663            .map(|_| handle.call_tool("barrier_tool", "{}"))
664            .collect();
665
666        // If execution is sequential, the first call will block at the barrier forever.
667        // We use a 1-second timeout to fail fast instead of hanging the test runner.
668        let result =
669            tokio::time::timeout(Duration::from_secs(1), futures::future::join_all(futures)).await;
670
671        assert!(
672            result.is_ok(),
673            "Tool execution deadlocked! Tools are executing sequentially instead of concurrently."
674        );
675
676        // All calls should succeed
677        for res in result.unwrap() {
678            assert!(res.is_ok(), "Tool call failed: {:?}", res);
679            assert_eq!(res.unwrap(), "done");
680        }
681    }
682
683    #[tokio::test]
684    pub async fn test_toolserver_write_while_tool_running() {
685        let started = Arc::new(tokio::sync::Notify::new());
686        let allow_finish = Arc::new(tokio::sync::Notify::new());
687
688        // Build server with the controlled tool that waits at a barrier during execution
689        let tool = MockControlledTool::new(started.clone(), allow_finish.clone());
690
691        let server = ToolServer::new().tool(tool);
692        let handle = server.run();
693
694        // Start tool call in background
695        let handle_clone = handle.clone();
696        let call_task =
697            tokio::spawn(async move { handle_clone.call_tool("controlled", "{}").await });
698
699        // Wait until we are strictly inside `call()`
700        started.notified().await;
701
702        // Try to write to the state (add a tool) while the tool call is mid-execution.
703        // If the read lock is incorrectly held across tool execution, this will deadlock.
704        let add_result =
705            tokio::time::timeout(Duration::from_secs(1), handle.add_tool(MockAddTool)).await;
706
707        assert!(
708            add_result.is_ok(),
709            "Writing to ToolServer deadlocked! The read lock is being held across tool execution."
710        );
711        assert!(add_result.unwrap().is_ok());
712
713        // Allow the background tool to finish and clean up
714        allow_finish.notify_one();
715        let call_result = call_task.await.unwrap();
716        assert_eq!(call_result.unwrap(), "42");
717    }
718
719    #[tokio::test]
720    pub async fn test_toolserver_parallel_dynamic_tool_fetching() {
721        // We expect exactly 2 parallel searches to hit the barrier at the same time
722        let barrier = Arc::new(tokio::sync::Barrier::new(2));
723
724        let index1 = BarrierMockToolIndex::new(barrier.clone(), "add");
725        let index2 = BarrierMockToolIndex::new(barrier.clone(), "subtract");
726
727        // Put both tools in the toolset so they resolve correctly
728        let mut toolset = ToolSet::default();
729        toolset.add_tool(MockAddTool);
730        toolset.add_tool(MockSubtractTool);
731
732        let server = ToolServer::new()
733            .dynamic_tools(1, index1, ToolSet::default())
734            .dynamic_tools(1, index2, toolset);
735
736        let handle = server.run();
737
738        // This will trigger a search across both indices.
739        // If fetched sequentially, the first index will wait at the barrier forever.
740        let get_defs = tokio::time::timeout(
741            std::time::Duration::from_secs(1),
742            handle.get_tool_defs(Some("do math".to_string())),
743        )
744        .await;
745
746        assert!(
747            get_defs.is_ok(),
748            "Dynamic tools were fetched sequentially! The first query deadlocked waiting for the second query to start."
749        );
750
751        let defs = get_defs.unwrap().unwrap();
752        assert_eq!(defs.len(), 2);
753
754        let tool_names: Vec<&str> = defs.iter().map(|t| t.name.as_str()).collect();
755        assert!(tool_names.contains(&"add"));
756        assert!(tool_names.contains(&"subtract"));
757    }
758
759    // --- call_with_extensions tests ---
760
761    #[derive(Clone)]
762    struct SessionId(String);
763
764    #[derive(serde::Deserialize, serde::Serialize)]
765    struct ExtensionsReader;
766
767    #[derive(Debug, thiserror::Error)]
768    #[error("context reader error")]
769    struct ExtensionsReaderError;
770
771    impl crate::tool::Tool for ExtensionsReader {
772        const NAME: &'static str = "context_reader";
773        type Error = ExtensionsReaderError;
774        type Args = serde_json::Value;
775        type Output = String;
776
777        fn description(&self) -> String {
778            "Reads SessionId from context".to_string()
779        }
780
781        fn parameters(&self) -> serde_json::Value {
782            serde_json::json!({"type": "object", "properties": {}})
783        }
784
785        async fn call(&self, _args: Self::Args) -> Result<Self::Output, Self::Error> {
786            Ok("no context".to_string())
787        }
788
789        async fn call_with_extensions(
790            &self,
791            _args: Self::Args,
792            extensions: &crate::tool::ToolCallExtensions,
793        ) -> Result<Self::Output, Self::Error> {
794            match extensions.get::<SessionId>() {
795                Some(session) => Ok(format!("session:{}", session.0)),
796                None => Ok("no session".to_string()),
797            }
798        }
799    }
800
801    #[tokio::test]
802    async fn test_call_tool_with_extensions_reaches_tool() {
803        let server = ToolServer::new().tool(ExtensionsReader);
804        let handle = server.run();
805
806        let mut extensions = crate::tool::ToolCallExtensions::new();
807        extensions.insert(SessionId("abc-123".to_string()));
808
809        let result = handle
810            .call_tool_with_extensions("context_reader", "{}", &extensions)
811            .await
812            .unwrap();
813
814        assert_eq!(result, "session:abc-123");
815    }
816
817    #[tokio::test]
818    async fn test_call_tool_without_extensions_uses_default() {
819        let server = ToolServer::new().tool(ExtensionsReader);
820        let handle = server.run();
821
822        let result = handle.call_tool("context_reader", "{}").await.unwrap();
823        assert_eq!(result, "no session");
824    }
825
826    #[tokio::test]
827    async fn test_tool_ignoring_extensions_still_works() {
828        let server = ToolServer::new().tool(MockAddTool);
829        let handle = server.run();
830
831        let mut extensions = crate::tool::ToolCallExtensions::new();
832        extensions.insert(SessionId("ignored".to_string()));
833
834        let args = serde_json::to_string(&serde_json::json!({"x": 3, "y": 7})).unwrap();
835        let result = handle
836            .call_tool_with_extensions("add", &args, &extensions)
837            .await
838            .unwrap();
839
840        assert_eq!(result, "10");
841    }
842
843    #[tokio::test]
844    async fn call_tool_structured_returns_success_for_a_known_tool() {
845        use crate::tool::{ToolCallExtensions, ToolOutcome};
846
847        let handle = ToolServer::new().tool(MockAddTool).run();
848        let args = serde_json::to_string(&serde_json::json!({"x": 2, "y": 5})).unwrap();
849        let result = handle
850            .call_tool_structured("add", &args, &ToolCallExtensions::EMPTY)
851            .await;
852
853        assert!(matches!(result.outcome, ToolOutcome::Success));
854        assert_eq!(result.model_output, "7");
855    }
856
857    #[tokio::test]
858    async fn call_tool_structured_classifies_a_missing_tool_as_not_found() {
859        use crate::tool::{ToolCallExtensions, ToolFailureKind, ToolOutcome};
860
861        let handle = ToolServer::new().tool(MockAddTool).run();
862        let result = handle
863            .call_tool_structured("does_not_exist", "{}", &ToolCallExtensions::EMPTY)
864            .await;
865
866        match result.outcome {
867            ToolOutcome::Error(failure) => assert_eq!(failure.kind, ToolFailureKind::NotFound),
868            other => panic!("expected a NotFound error outcome, got {other:?}"),
869        }
870        assert!(result.model_output.contains("does_not_exist"));
871    }
872}