Skip to main content

reson_agentic/runtime/
mod.rs

1//! Runtime - Core execution environment for agentic functions
2//!
3//! The Runtime is the main interface for Reson. It manages:
4//! - LLM client lifecycle and API calls
5//! - Tool registration and execution
6//! - Message history and accumulators
7//! - Structured output parsing
8
9#![allow(clippy::too_many_arguments)]
10
11use std::collections::HashMap;
12use std::sync::Arc;
13use tokio::sync::RwLock;
14
15use crate::error::{Error, Result};
16use crate::parsers::{Deserializable, ParsedTool, ToolConstructor};
17use crate::schema::ToolParametersSchema;
18use crate::types::ReasoningSegment;
19use crate::utils::ConversationMessage;
20use futures::future::BoxFuture;
21
22pub mod decorators;
23pub mod inference;
24
25/// Accumulated state during runtime execution
26#[derive(Debug, Default, Clone)]
27pub struct Accumulators {
28    raw_response: Vec<String>,
29    reasoning: Vec<String>,
30    reasoning_segments: Vec<ReasoningSegment>,
31    current_reasoning_segment: Option<ReasoningSegment>,
32}
33
34/// Stored schema information for a tool
35#[derive(Debug, Clone)]
36pub struct ToolSchemaInfo {
37    pub name: String,
38    pub description: String,
39    pub fields: Vec<crate::parsers::FieldDescription>,
40    pub parameters: ToolParametersSchema,
41}
42
43/// Runtime - Main execution environment for agentic functions
44pub struct Runtime {
45    // Public configuration
46    pub model: Option<String>,
47    pub api_key: Option<String>,
48    pub used: bool,
49
50    // Private state (using interior mutability)
51    tools: Arc<RwLock<HashMap<String, ToolFunction>>>,
52    tool_types: Arc<RwLock<HashMap<String, String>>>, // tool_name -> type_name mapping
53    tool_schemas: Arc<RwLock<HashMap<String, ToolSchemaInfo>>>, // tool_name -> schema info
54    tool_constructors: Arc<RwLock<HashMap<String, Arc<ToolConstructor>>>>, // For NativeToolParser
55    default_prompt: Arc<RwLock<String>>,
56    return_type: Arc<RwLock<Option<String>>>, // Store type name as string
57    accumulators: Arc<RwLock<Accumulators>>,
58    #[allow(dead_code)]
59    messages: Arc<RwLock<Vec<ConversationMessage>>>,
60    current_call_args: Arc<RwLock<Option<HashMap<String, serde_json::Value>>>>,
61}
62
63/// Wrapper for tool functions (sync or async)
64///
65/// Tool functions receive raw JSON args and are responsible for deserializing
66/// into their expected type. The `tool<T, F>()` method handles this automatically
67/// by wrapping typed handlers.
68pub enum ToolFunction {
69    Sync(Box<dyn Fn(serde_json::Value) -> Result<String> + Send + Sync>),
70    Async(
71        Box<
72            dyn Fn(serde_json::Value) -> futures::future::BoxFuture<'static, Result<String>>
73                + Send
74                + Sync,
75        >,
76    ),
77}
78
79/// Parameters for `Runtime::run()` and `Runtime::run_stream()`
80#[derive(Debug, Default, Clone)]
81pub struct RunParams {
82    pub prompt: Option<String>,
83    pub system: Option<String>,
84    pub history: Option<Vec<ConversationMessage>>,
85    pub output_type: Option<String>,
86    pub output_schema: Option<serde_json::Value>,
87    pub temperature: Option<f32>,
88    pub top_p: Option<f32>,
89    pub max_tokens: Option<u32>,
90    pub model: Option<String>,
91    pub api_key: Option<String>,
92    pub timeout: Option<std::time::Duration>,
93}
94
95/// Metadata about a tool call for execution context
96#[derive(Debug, Clone)]
97pub struct ToolCallContext {
98    pub tool_name: String,
99    pub tool_use_id: String,
100}
101
102impl Runtime {
103    /// Create a new Runtime with default memory storage
104    pub fn new() -> Self {
105        Self {
106            model: None,
107            api_key: None,
108            used: false,
109            tools: Arc::new(RwLock::new(HashMap::new())),
110            tool_types: Arc::new(RwLock::new(HashMap::new())),
111            tool_schemas: Arc::new(RwLock::new(HashMap::new())),
112            tool_constructors: Arc::new(RwLock::new(HashMap::new())),
113            default_prompt: Arc::new(RwLock::new(String::new())),
114            return_type: Arc::new(RwLock::new(None)),
115            accumulators: Arc::new(RwLock::new(Accumulators::default())),
116            messages: Arc::new(RwLock::new(Vec::new())),
117            current_call_args: Arc::new(RwLock::new(None)),
118        }
119    }
120
121    /// Create a Runtime with specific configuration
122    pub fn with_config(model: Option<String>, api_key: Option<String>) -> Self {
123        Self {
124            model,
125            api_key,
126            used: false,
127            tools: Arc::new(RwLock::new(HashMap::new())),
128            tool_types: Arc::new(RwLock::new(HashMap::new())),
129            tool_schemas: Arc::new(RwLock::new(HashMap::new())),
130            tool_constructors: Arc::new(RwLock::new(HashMap::new())),
131            default_prompt: Arc::new(RwLock::new(String::new())),
132            return_type: Arc::new(RwLock::new(None)),
133            accumulators: Arc::new(RwLock::new(Accumulators::default())),
134            messages: Arc::new(RwLock::new(Vec::new())),
135            current_call_args: Arc::new(RwLock::new(None)),
136        }
137    }
138
139    /// Register a tool function
140    ///
141    /// # Arguments
142    /// * `name` - Tool name for LLM
143    /// * `tool_fn` - Sync or async function
144    /// * `tool_type` - Optional type name for marshalling
145    pub async fn register_tool(
146        &self,
147        name: impl Into<String>,
148        tool_fn: ToolFunction,
149        tool_type: Option<String>,
150    ) -> Result<()> {
151        let name = name.into();
152
153        // Store tool function
154        let mut tools = self.tools.write().await;
155        if tools.contains_key(&name) {
156            return Err(Error::NonRetryable(format!(
157                "Tool '{}' is already registered",
158                name
159            )));
160        }
161        tools.insert(name.clone(), tool_fn);
162        drop(tools);
163
164        // Store tool type if provided
165        if let Some(type_name) = tool_type {
166            let mut tool_types = self.tool_types.write().await;
167            tool_types.insert(name, type_name);
168        }
169
170        Ok(())
171    }
172
173    /// Register a tool function with schema information
174    ///
175    /// This method is useful when you have a tool struct that provides its schema
176    /// via `#[derive(Tool)]`, and you want to register it with a handler function.
177    ///
178    /// # Arguments
179    /// * `name` - Tool name for LLM
180    /// * `description` - Tool description
181    /// * `schema` - JSON schema for the tool parameters (from Tool::schema())
182    /// * `tool_fn` - Sync or async function to handle the tool call
183    ///
184    /// # Example
185    /// ```ignore
186    /// runtime.register_tool_with_schema(
187    ///     MyTool::tool_name(),
188    ///     MyTool::description(),
189    ///     MyTool::schema(),
190    ///     ToolFunction::Sync(Box::new(|args| {
191    ///         // Handle the tool call
192    ///         Ok("result".to_string())
193    ///     })),
194    /// ).await?;
195    /// ```
196    pub async fn register_tool_with_schema(
197        &self,
198        name: impl Into<String>,
199        description: impl Into<String>,
200        schema: serde_json::Value,
201        tool_fn: ToolFunction,
202    ) -> Result<()> {
203        let name = name.into();
204        let description = description.into();
205
206        // Store tool function
207        let mut tools = self.tools.write().await;
208        if tools.contains_key(&name) {
209            return Err(Error::NonRetryable(format!(
210                "Tool '{}' is already registered",
211                name
212            )));
213        }
214        tools.insert(name.clone(), tool_fn);
215        drop(tools);
216
217        // Parse and store full schema information while preserving
218        // flat field metadata for introspection and existing callers.
219        let parameters = ToolParametersSchema::from_json_schema(&schema)?;
220        let fields = parameters.top_level_field_descriptions();
221        let schema_info = ToolSchemaInfo {
222            name: name.clone(),
223            description,
224            fields,
225            parameters,
226        };
227        let mut schemas = self.tool_schemas.write().await;
228        schemas.insert(name, schema_info);
229
230        Ok(())
231    }
232
233    /// Register a tool with type and handler (Python: runtime.tool(fn, name=..., tool_type=...))
234    ///
235    /// This is the primary API for registering tools for native tool calling.
236    /// The type parameter T must implement Deserializable. The handler receives
237    /// the typed struct directly - deserialization is handled automatically.
238    ///
239    /// # Arguments
240    /// * `handler` - Async function that receives the typed struct T
241    /// * `name` - Optional tool name (defaults to type name if None)
242    ///
243    /// # Example
244    /// ```ignore
245    /// #[derive(Deserialize, Serialize)]
246    /// struct WeatherQuery {
247    ///     city: String,
248    ///     units: Option<String>,
249    /// }
250    /// impl Deserializable for WeatherQuery { ... }
251    ///
252    /// // Handler receives WeatherQuery directly, not raw JSON
253    /// runtime.tool::<WeatherQuery, _>(|query| Box::pin(async move {
254    ///     Ok(format!("Weather in {}: Sunny", query.city))
255    /// }), Some("get_weather")).await?;
256    /// ```
257    pub async fn tool<T, F>(&self, handler: F, name: Option<&str>) -> Result<()>
258    where
259        T: Deserializable + serde::Serialize + 'static,
260        F: Fn(T) -> BoxFuture<'static, Result<String>> + Send + Sync + 'static,
261    {
262        // Get tool name - either from parameter or type name
263        let type_name = std::any::type_name::<T>();
264        let tool_name_str =
265            name.unwrap_or_else(|| type_name.split("::").last().unwrap_or(type_name));
266        let tool_name = tool_name_str.to_string();
267
268        // Check for duplicate registration
269        let mut tools = self.tools.write().await;
270        if tools.contains_key(&tool_name) {
271            return Err(Error::NonRetryable(format!(
272                "Tool '{}' is already registered",
273                tool_name
274            )));
275        }
276
277        // Wrap the typed handler to deserialize JSON -> T before calling
278        let handler = Arc::new(handler);
279        let wrapped_handler = Box::new(move |json_value: serde_json::Value| {
280            let handler = handler.clone();
281            Box::pin(async move {
282                // Deserialize JSON into the typed struct T
283                let typed_args: T = T::from_partial(json_value)?;
284                // Call the handler with the typed struct
285                handler(typed_args).await
286            }) as BoxFuture<'static, Result<String>>
287        });
288
289        tools.insert(tool_name.clone(), ToolFunction::Async(wrapped_handler));
290        drop(tools);
291
292        // Store type name for introspection
293        let mut tool_types = self.tool_types.write().await;
294        tool_types.insert(tool_name.clone(), type_name.to_string());
295        drop(tool_types);
296
297        // Extract and store schema information from Deserializable type
298        let field_descriptions = T::field_descriptions();
299        let parameters = ToolParametersSchema::from_field_descriptions(&field_descriptions);
300        let schema_info = ToolSchemaInfo {
301            name: tool_name.clone(),
302            description: format!("Tool: {}", tool_name), // Default description
303            fields: field_descriptions,
304            parameters,
305        };
306        let mut schemas = self.tool_schemas.write().await;
307        schemas.insert(tool_name.clone(), schema_info);
308        drop(schemas);
309
310        // Store constructor for NativeToolParser (streaming use case)
311        let tool_name_clone = tool_name.clone();
312        let constructor: ToolConstructor = Box::new(move |json: serde_json::Value| {
313            T::from_partial(json.clone()).map(|tool| {
314                ParsedTool {
315                    tool_name: tool_name_clone.clone(),
316                    tool_use_id: String::new(), // Will be set by parser
317                    value: serde_json::to_value(&tool).unwrap(),
318                }
319            })
320        });
321
322        let mut constructors = self.tool_constructors.write().await;
323        constructors.insert(tool_name.clone(), Arc::new(constructor));
324
325        Ok(())
326    }
327
328    /// Get a NativeToolParser instance with all registered tool constructors
329    ///
330    /// This parser can be used during streaming to dynamically construct ParsedTool
331    /// instances from tool call deltas.
332    pub async fn get_parser(&self) -> crate::parsers::NativeToolParser {
333        let constructors = self.tool_constructors.read().await;
334        crate::parsers::NativeToolParser::new(constructors.clone())
335    }
336
337    /// Get tool schema information for all registered tools
338    pub async fn get_tool_schemas(&self) -> HashMap<String, ToolSchemaInfo> {
339        let schemas = self.tool_schemas.read().await;
340        schemas.clone()
341    }
342
343    /// Execute a non-streaming LLM call
344    pub async fn run(&mut self, params: RunParams) -> Result<serde_json::Value> {
345        // Mark as used
346        self.used = true;
347
348        // Clear accumulators
349        self.clear_raw_response().await;
350        self.clear_reasoning().await;
351
352        // Get effective values
353        let prompt_text = match params.prompt {
354            Some(p) => p,
355            None => self.default_prompt.read().await.clone(),
356        };
357
358        let effective_model = params
359            .model
360            .or_else(|| self.model.clone())
361            .ok_or_else(|| Error::NonRetryable("No model specified".to_string()))?;
362
363        let effective_api_key = params.api_key.or_else(|| self.api_key.clone());
364
365        // Call inference utilities
366        let result = inference::call_llm(
367            Some(&prompt_text),
368            &effective_model,
369            self.tools.clone(),
370            self.tool_schemas.clone(),
371            params.output_type,
372            params.output_schema,
373            effective_api_key.as_deref(),
374            params.system.as_deref(),
375            params.history,
376            params.temperature,
377            params.top_p,
378            params.max_tokens,
379            params.timeout,
380            self.current_call_args.clone(),
381        )
382        .await?;
383
384        // Update accumulators
385        if let Some(raw) = &result.raw_response {
386            let mut acc = self.accumulators.write().await;
387            acc.raw_response.push(raw.clone());
388        }
389
390        if let Some(reasoning) = &result.reasoning {
391            let mut acc = self.accumulators.write().await;
392            acc.reasoning.push(reasoning.clone());
393        }
394
395        Ok(result.parsed_value)
396    }
397
398    /// Execute a streaming LLM call
399    ///
400    /// Returns an async stream of (chunk_type, chunk_value) tuples
401    pub async fn run_stream(
402        &mut self,
403        params: RunParams,
404    ) -> Result<impl futures::stream::Stream<Item = Result<(String, serde_json::Value)>>> {
405        // Mark as used
406        self.used = true;
407
408        // Clear accumulators
409        self.clear_raw_response().await;
410        self.clear_reasoning().await;
411        self.clear_reasoning_segments().await;
412
413        // Get effective values
414        let prompt_text = match params.prompt {
415            Some(p) => p,
416            None => self.default_prompt.read().await.clone(),
417        };
418
419        let effective_model = params
420            .model
421            .or_else(|| self.model.clone())
422            .ok_or_else(|| Error::NonRetryable("No model specified".to_string()))?;
423
424        let effective_api_key = params.api_key.or_else(|| self.api_key.clone());
425
426        // Call streaming inference
427        inference::call_llm_stream(
428            Some(&prompt_text),
429            &effective_model,
430            self.tools.clone(),
431            self.tool_schemas.clone(),
432            params.output_type,
433            params.output_schema,
434            effective_api_key.as_deref(),
435            params.system.as_deref(),
436            params.history,
437            params.temperature,
438            params.top_p,
439            params.max_tokens,
440            params.timeout,
441            self.current_call_args.clone(),
442            self.accumulators.clone(),
443        )
444        .await
445    }
446
447    /// Check if a result is a tool call
448    pub fn is_tool_call(&self, result: &serde_json::Value) -> bool {
449        // Check for _tool_name field in JSON
450        result.get("_tool_name").and_then(|v| v.as_str()).is_some()
451    }
452
453    /// Get tool name from result
454    pub fn get_tool_name(&self, result: &serde_json::Value) -> Option<String> {
455        result
456            .get("_tool_name")
457            .and_then(|v| v.as_str())
458            .map(|s| s.to_string())
459    }
460
461    /// Execute a tool call
462    pub async fn execute_tool(&self, tool_result: &serde_json::Value) -> Result<String> {
463        let tool_name = self
464            .get_tool_name(tool_result)
465            .ok_or_else(|| Error::NonRetryable("No tool name in result".to_string()))?;
466
467        let tools = self.tools.read().await;
468        let tool_fn = tools
469            .get(&tool_name)
470            .ok_or_else(|| Error::NonRetryable(format!("Tool '{}' not found", tool_name)))?;
471
472        // Extract arguments
473        let args = tool_result.clone();
474
475        // Execute based on function type
476        match tool_fn {
477            ToolFunction::Sync(f) => f(args),
478            ToolFunction::Async(f) => f(args).await,
479        }
480    }
481
482    /// Get accumulated raw response
483    pub async fn raw_response(&self) -> String {
484        let acc = self.accumulators.read().await;
485        acc.raw_response.join("")
486    }
487
488    /// Get accumulated reasoning
489    pub async fn reasoning(&self) -> String {
490        let acc = self.accumulators.read().await;
491        acc.reasoning.join("")
492    }
493
494    /// Get reasoning segments
495    pub async fn reasoning_segments(&self) -> Vec<ReasoningSegment> {
496        let acc = self.accumulators.read().await;
497        acc.reasoning_segments.clone()
498    }
499
500    /// Clear raw response accumulator
501    pub async fn clear_raw_response(&self) {
502        let mut acc = self.accumulators.write().await;
503        acc.raw_response.clear();
504    }
505
506    /// Clear reasoning accumulator
507    pub async fn clear_reasoning(&self) {
508        let mut acc = self.accumulators.write().await;
509        acc.reasoning.clear();
510    }
511
512    /// Clear reasoning segments
513    pub async fn clear_reasoning_segments(&self) {
514        let mut acc = self.accumulators.write().await;
515        acc.reasoning_segments.clear();
516        acc.current_reasoning_segment = None;
517    }
518
519    /// Set default prompt (from function docstring)
520    pub async fn set_default_prompt(&self, prompt: impl Into<String>) {
521        let mut default_prompt = self.default_prompt.write().await;
522        *default_prompt = prompt.into();
523    }
524
525    /// Set return type
526    pub async fn set_return_type(&self, type_name: Option<String>) {
527        let mut return_type = self.return_type.write().await;
528        *return_type = type_name;
529    }
530
531    /// Set current call arguments (for prompt enhancement)
532    pub async fn set_current_call_args(&self, args: Option<HashMap<String, serde_json::Value>>) {
533        let mut current_args = self.current_call_args.write().await;
534        *current_args = args;
535    }
536
537    /// Connect to an MCP server and register all its tools into this runtime.
538    ///
539    /// Auto-detects transport from the URI:
540    /// - `http://` or `https://` → HTTP streaming
541    /// - `ws://` or `wss://` → WebSocket
542    /// - Anything else → stdio (treated as a command to spawn)
543    ///
544    /// Discovered tools become available alongside locally registered tools.
545    ///
546    /// # Example
547    /// ```rust,no_run
548    /// # use reson_agentic::runtime::Runtime;
549    /// # async fn example() -> reson_agentic::error::Result<()> {
550    /// # let runtime = Runtime::new();
551    /// runtime.mcp("http://localhost:8080/mcp").await?;
552    /// runtime.mcp("npx @modelcontextprotocol/server-filesystem /tmp").await?;
553    /// # Ok(())
554    /// # }
555    /// ```
556    #[cfg(feature = "mcp")]
557    pub async fn mcp(&self, uri: impl Into<String>) -> Result<()> {
558        let _ = crate::mcp::connect_and_register(self, &uri.into(), None).await?;
559        Ok(())
560    }
561
562    /// Connect to an MCP server and register its tools with a namespace prefix.
563    ///
564    /// Tools are registered as `{label}_{tool_name}`. The original name is
565    /// still used when calling the remote server. Use this to avoid conflicts
566    /// when connecting to multiple MCP servers that expose tools with the
567    /// same name.
568    ///
569    /// # Example
570    /// ```rust,no_run
571    /// # use reson_agentic::runtime::Runtime;
572    /// # async fn example() -> reson_agentic::error::Result<()> {
573    /// # let runtime = Runtime::new();
574    /// runtime.mcp_as("http://server1:8080", "s1").await?; // s1_search, s1_read
575    /// runtime.mcp_as("http://server2:8080", "s2").await?; // s2_search, s2_write
576    /// # Ok(())
577    /// # }
578    /// ```
579    #[cfg(feature = "mcp")]
580    pub async fn mcp_as(&self, uri: impl Into<String>, label: &str) -> Result<()> {
581        let _ = crate::mcp::connect_and_register(self, &uri.into(), Some(label)).await?;
582        Ok(())
583    }
584}
585
586impl Default for Runtime {
587    fn default() -> Self {
588        Self::new()
589    }
590}
591
592#[cfg(test)]
593mod tests {
594    use super::*;
595
596    #[tokio::test]
597    async fn test_runtime_new() {
598        let runtime = Runtime::new();
599        assert!(!runtime.used);
600        assert!(runtime.model.is_none());
601    }
602
603    #[tokio::test]
604    async fn test_runtime_with_config() {
605        let runtime =
606            Runtime::with_config(Some("test-model".to_string()), Some("test-key".to_string()));
607
608        assert_eq!(runtime.model, Some("test-model".to_string()));
609        assert_eq!(runtime.api_key, Some("test-key".to_string()));
610    }
611
612    #[tokio::test]
613    async fn test_register_tool() {
614        let runtime = Runtime::new();
615
616        let tool_fn = ToolFunction::Sync(Box::new(|_args| Ok("result".to_string())));
617
618        runtime
619            .register_tool("test_tool", tool_fn, None)
620            .await
621            .unwrap();
622
623        let tools = runtime.tools.read().await;
624        assert!(tools.contains_key("test_tool"));
625    }
626
627    #[tokio::test]
628    async fn test_register_duplicate_tool() {
629        let runtime = Runtime::new();
630
631        let tool_fn1 = ToolFunction::Sync(Box::new(|_args| Ok("result1".to_string())));
632        let tool_fn2 = ToolFunction::Sync(Box::new(|_args| Ok("result2".to_string())));
633
634        runtime
635            .register_tool("test_tool", tool_fn1, None)
636            .await
637            .unwrap();
638
639        let result = runtime.register_tool("test_tool", tool_fn2, None).await;
640        assert!(result.is_err());
641    }
642
643    #[tokio::test]
644    async fn test_is_tool_call() {
645        let runtime = Runtime::new();
646
647        let tool_call = serde_json::json!({
648            "_tool_name": "test_tool",
649            "arg1": "value1"
650        });
651
652        assert!(runtime.is_tool_call(&tool_call));
653
654        let not_tool_call = serde_json::json!({
655            "result": "value"
656        });
657
658        assert!(!runtime.is_tool_call(&not_tool_call));
659    }
660
661    #[tokio::test]
662    async fn test_get_tool_name() {
663        let runtime = Runtime::new();
664
665        let tool_call = serde_json::json!({
666            "_tool_name": "my_tool",
667            "arg1": "value1"
668        });
669
670        assert_eq!(
671            runtime.get_tool_name(&tool_call),
672            Some("my_tool".to_string())
673        );
674    }
675
676    #[tokio::test]
677    async fn test_execute_tool_sync() {
678        let runtime = Runtime::new();
679
680        let tool_fn = ToolFunction::Sync(Box::new(|args| {
681            let name = args.get("name").and_then(|v| v.as_str()).unwrap_or("world");
682            Ok(format!("Hello, {}!", name))
683        }));
684
685        runtime.register_tool("greet", tool_fn, None).await.unwrap();
686
687        let tool_call = serde_json::json!({
688            "_tool_name": "greet",
689            "name": "Alice"
690        });
691
692        let result = runtime.execute_tool(&tool_call).await.unwrap();
693        assert_eq!(result, "Hello, Alice!");
694    }
695
696    #[tokio::test]
697    async fn test_accumulators() {
698        let runtime = Runtime::new();
699
700        // Initially empty
701        assert_eq!(runtime.raw_response().await, "");
702        assert_eq!(runtime.reasoning().await, "");
703
704        // Add some data
705        {
706            let mut acc = runtime.accumulators.write().await;
707            acc.raw_response.push("Hello ".to_string());
708            acc.raw_response.push("World".to_string());
709            acc.reasoning.push("Think: ".to_string());
710            acc.reasoning.push("Answer".to_string());
711        }
712
713        assert_eq!(runtime.raw_response().await, "Hello World");
714        assert_eq!(runtime.reasoning().await, "Think: Answer");
715
716        // Clear
717        runtime.clear_raw_response().await;
718        runtime.clear_reasoning().await;
719
720        assert_eq!(runtime.raw_response().await, "");
721        assert_eq!(runtime.reasoning().await, "");
722    }
723
724    #[tokio::test]
725    #[ignore] // TODO: Requires Storage trait refactor for interior mutability
726    async fn test_context() {
727        // TODO: Implement when context() method is added to Runtime
728    }
729
730    // Test types for tool registration
731    #[derive(Debug, serde::Serialize, serde::Deserialize)]
732    struct WeatherQuery {
733        #[serde(default)]
734        location: String,
735        #[serde(default)]
736        unit: Option<String>,
737    }
738
739    impl crate::parsers::Deserializable for WeatherQuery {
740        fn from_partial(partial: serde_json::Value) -> crate::error::Result<Self> {
741            serde_json::from_value(partial)
742                .map_err(|e| crate::error::Error::NonRetryable(format!("Parse error: {}", e)))
743        }
744
745        fn validate_complete(&self) -> crate::error::Result<()> {
746            if self.location.is_empty() {
747                return Err(crate::error::Error::NonRetryable(
748                    "location is required".to_string(),
749                ));
750            }
751            Ok(())
752        }
753
754        fn field_descriptions() -> Vec<crate::parsers::FieldDescription> {
755            vec![
756                crate::parsers::FieldDescription {
757                    name: "location".to_string(),
758                    field_type: "string".to_string(),
759                    description: "The city to get weather for".to_string(),
760                    required: true,
761                },
762                crate::parsers::FieldDescription {
763                    name: "unit".to_string(),
764                    field_type: "string".to_string(),
765                    description: "Temperature unit (celsius or fahrenheit)".to_string(),
766                    required: false,
767                },
768            ]
769        }
770    }
771
772    #[tokio::test]
773    async fn test_tool_registration_with_schema() {
774        use futures::future::BoxFuture;
775
776        let runtime = Runtime::new();
777
778        // Register tool with type parameter - handler receives WeatherQuery directly
779        runtime
780            .tool::<WeatherQuery, _>(
781                |query| -> BoxFuture<'static, crate::error::Result<String>> {
782                    Box::pin(async move { Ok(format!("Weather in {}: Sunny", query.location)) })
783                },
784                Some("get_weather"),
785            )
786            .await
787            .unwrap();
788
789        // Check that tool is registered
790        let tools = runtime.tools.read().await;
791        assert!(tools.contains_key("get_weather"));
792        drop(tools);
793
794        // Check that schema info is captured
795        let schemas = runtime.tool_schemas.read().await;
796        let schema_info = schemas.get("get_weather").unwrap();
797        assert_eq!(schema_info.name, "get_weather");
798        assert_eq!(schema_info.fields.len(), 2);
799        assert_eq!(schema_info.fields[0].name, "location");
800        assert!(schema_info.fields[0].required);
801        assert_eq!(schema_info.fields[1].name, "unit");
802        assert!(!schema_info.fields[1].required);
803    }
804
805    #[tokio::test]
806    async fn test_tool_schema_generation() {
807        use futures::future::BoxFuture;
808
809        let runtime = Runtime::new();
810
811        // Register tool - handler receives WeatherQuery directly
812        runtime
813            .tool::<WeatherQuery, _>(
814                |_query| -> BoxFuture<'static, crate::error::Result<String>> {
815                    Box::pin(async move { Ok("Sunny".to_string()) })
816                },
817                Some("get_weather"),
818            )
819            .await
820            .unwrap();
821
822        // Get tool schemas and verify structure
823        let schemas = runtime.get_tool_schemas().await;
824        assert_eq!(schemas.len(), 1);
825
826        let weather_schema = schemas.get("get_weather").unwrap();
827        assert!(weather_schema
828            .fields
829            .iter()
830            .any(|f| f.name == "location" && f.required));
831        assert!(weather_schema
832            .fields
833            .iter()
834            .any(|f| f.name == "unit" && !f.required));
835        assert_eq!(
836            weather_schema.parameters.to_json_schema()["properties"]["location"]["type"],
837            "string"
838        );
839    }
840
841    #[tokio::test]
842    async fn test_register_tool_with_schema_preserves_nested_structure() {
843        let runtime = Runtime::new();
844
845        runtime
846            .register_tool_with_schema(
847                "write_thread",
848                "Write a thread",
849                serde_json::json!({
850                    "type": "object",
851                    "properties": {
852                        "items": {
853                            "type": "array",
854                            "items": {
855                                "$ref": "#/$defs/ThreadItem"
856                            }
857                        }
858                    },
859                    "required": ["items"],
860                    "$defs": {
861                        "ThreadItem": {
862                            "type": "object",
863                            "properties": {
864                                "text": { "type": "string" },
865                                "image_ids": {
866                                    "type": "array",
867                                    "items": { "type": "integer" }
868                                }
869                            },
870                            "required": ["text"]
871                        }
872                    }
873                }),
874                ToolFunction::Sync(Box::new(|args| Ok(args.to_string()))),
875            )
876            .await
877            .unwrap();
878
879        let schemas = runtime.get_tool_schemas().await;
880        let write_thread = schemas.get("write_thread").unwrap();
881        let json = write_thread.parameters.to_json_schema();
882        assert_eq!(
883            json["properties"]["items"]["items"]["properties"]["text"]["type"],
884            "string"
885        );
886        assert_eq!(
887            json["properties"]["items"]["items"]["properties"]["image_ids"]["items"]["type"],
888            "integer"
889        );
890    }
891
892    #[tokio::test]
893    async fn test_tool_hydration_and_execution() {
894        use futures::future::BoxFuture;
895
896        let runtime = Runtime::new();
897
898        // Register tool - handler receives WeatherQuery, not raw JSON
899        runtime
900            .tool::<WeatherQuery, _>(
901                |query| -> BoxFuture<'static, crate::error::Result<String>> {
902                    Box::pin(async move {
903                        // Handler receives the deserialized WeatherQuery struct
904                        let unit = query.unit.unwrap_or_else(|| "celsius".to_string());
905                        Ok(format!(
906                            "Weather in {} ({}): Sunny, 22°",
907                            query.location, unit
908                        ))
909                    })
910                },
911                Some("get_weather"),
912            )
913            .await
914            .unwrap();
915
916        // Simulate a tool call from LLM with JSON args
917        let tool_call = serde_json::json!({
918            "_tool_name": "get_weather",
919            "location": "Paris",
920            "unit": "fahrenheit"
921        });
922
923        // Execute the tool - it should deserialize JSON -> WeatherQuery -> call handler
924        let result = runtime.execute_tool(&tool_call).await.unwrap();
925        assert_eq!(result, "Weather in Paris (fahrenheit): Sunny, 22°");
926    }
927
928    #[tokio::test]
929    async fn test_tool_hydration_with_defaults() {
930        use futures::future::BoxFuture;
931
932        let runtime = Runtime::new();
933
934        // Register tool
935        runtime
936            .tool::<WeatherQuery, _>(
937                |query| -> BoxFuture<'static, crate::error::Result<String>> {
938                    Box::pin(async move {
939                        let unit = query.unit.unwrap_or_else(|| "celsius".to_string());
940                        Ok(format!("{} in {}", unit, query.location))
941                    })
942                },
943                Some("get_weather"),
944            )
945            .await
946            .unwrap();
947
948        // Tool call with optional field missing
949        let tool_call = serde_json::json!({
950            "_tool_name": "get_weather",
951            "location": "Tokyo"
952            // unit is not provided, should use default
953        });
954
955        let result = runtime.execute_tool(&tool_call).await.unwrap();
956        assert_eq!(result, "celsius in Tokyo");
957    }
958}