Skip to main content

netget/llm/actions/
tools.rs

1//! Tool call definitions and execution for LLM
2//!
3//! This module provides tools that the LLM can invoke to gather information
4//! before generating its final response. Tools include file reading and web search.
5
6use super::{ActionDefinition, Parameter};
7use anyhow::{Context, Result};
8use regex::Regex;
9use serde::{Deserialize, Serialize};
10use serde_json::json;
11use std::path::PathBuf;
12use tracing::{debug, error, warn};
13
14/// Maximum file size to read (1MB)
15const MAX_FILE_SIZE: u64 = 1024 * 1024;
16
17/// Default number of lines for head/tail
18const DEFAULT_LINES: usize = 50;
19
20/// Tool actions that the LLM can invoke
21#[derive(Debug, Clone, Deserialize, Serialize)]
22#[serde(tag = "type", rename_all = "snake_case")]
23pub enum ToolAction {
24    /// Read a file from the filesystem
25    ReadFile {
26        /// Path to the file (relative to working directory or absolute)
27        path: String,
28
29        /// Read mode: full, head, tail, or grep
30        #[serde(default = "default_read_mode")]
31        mode: String,
32
33        /// Number of lines for head/tail mode
34        #[serde(default)]
35        lines: Option<usize>,
36
37        /// Regex pattern for grep mode
38        #[serde(default)]
39        pattern: Option<String>,
40
41        /// Lines of context before match (grep -B)
42        #[serde(default)]
43        context_before: Option<usize>,
44
45        /// Lines of context after match (grep -A)
46        #[serde(default)]
47        context_after: Option<usize>,
48    },
49
50    /// Fetch a URL or search the web using DuckDuckGo
51    WebSearch {
52        /// URL to fetch or search query
53        query: String,
54    },
55
56    /// Get detailed documentation for a specific protocol (DEPRECATED - use ReadDocumentation)
57    ReadBaseStackDocs {
58        /// Protocol name (e.g., "http", "ssh", "tor")
59        protocol: String,
60    },
61
62    /// Get detailed server protocol documentation (DEPRECATED - use ReadDocumentation)
63    ReadServerDocumentation {
64        /// Server protocol names (e.g., ["HTTP", "SSH", "TOR"])
65        #[serde(default)]
66        protocols: Vec<String>,
67        /// Single protocol (backwards compatibility, use protocols instead)
68        #[serde(default)]
69        protocol: Option<String>,
70    },
71
72    /// Get detailed client protocol documentation (DEPRECATED - use ReadDocumentation)
73    ReadClientDocumentation {
74        /// Client protocol names (e.g., ["http", "ssh", "tor"])
75        #[serde(default)]
76        protocols: Vec<String>,
77        /// Single protocol (backwards compatibility, use protocols instead)
78        #[serde(default)]
79        protocol: Option<String>,
80    },
81
82    /// Get detailed protocol documentation for servers and/or clients
83    ReadDocumentation {
84        /// Protocol names to look up (e.g., ["http", "ssh", "dns"])
85        #[serde(default)]
86        protocols: Vec<String>,
87        /// Single protocol (backwards compatibility)
88        #[serde(default)]
89        protocol: Option<String>,
90    },
91
92    /// List available models from Ollama
93    ListModels,
94
95    /// Generate random data (strings, numbers, UUIDs, etc.)
96    GenerateRandom {
97        /// Type of random data to generate
98        data_type: String,
99
100        /// Optional: length for strings/arrays
101        #[serde(default)]
102        length: Option<usize>,
103
104        /// Optional: minimum value for numbers
105        #[serde(default)]
106        min: Option<f64>,
107
108        /// Optional: maximum value for numbers
109        #[serde(default)]
110        max: Option<f64>,
111
112        /// Optional: character set for strings (alphanumeric, hex, base64, letters, digits)
113        #[serde(default)]
114        charset: Option<String>,
115
116        /// Optional: array of choices to pick from
117        #[serde(default)]
118        choices: Option<Vec<serde_json::Value>>,
119
120        /// Optional: number of items to pick from choices
121        #[serde(default)]
122        count: Option<usize>,
123    },
124
125    /// List all scheduled tasks
126    ListTasks,
127
128    /// Execute a SQL query on a database
129    #[cfg(feature = "sqlite")]
130    ExecuteSql {
131        /// Database ID (from create_database response or list_databases)
132        database_id: u32,
133
134        /// SQL query to execute
135        query: String,
136    },
137
138    /// List all active SQLite databases with their schemas
139    #[cfg(feature = "sqlite")]
140    ListDatabases,
141}
142
143fn default_read_mode() -> String {
144    "full".to_string()
145}
146
147/// The single source of truth for which action names are *tools*.
148///
149/// A tool gathers information, returns its result to the model, and causes the model to be
150/// invoked again with that result. Everything that has to agree on "is this a tool?" reads
151/// this list:
152///
153/// * [`ToolAction::is_tool_action`] — routes a returned action to the tool loop at runtime.
154/// * [`ToolAction::from_json`] — accepts a tool name for parsing.
155/// * [`crate::llm::actions::ActionDefinition::is_tool`] — partitions the prompt into the
156///   "Available Tools" and "Available Actions" sections, whose boilerplate makes opposite
157///   promises about re-invocation.
158///
159/// Keeping these on one list is what prevents the prompt from advertising a re-invocation
160/// the runtime will not perform (or vice versa). Adding a variant to [`ToolAction`] means
161/// adding its name here.
162pub const TOOL_ACTION_NAMES: &[&str] = &[
163    "read_file",
164    "web_search",
165    "read_base_stack_docs",
166    "read_server_documentation",
167    "read_client_documentation",
168    "read_documentation",
169    "list_models",
170    "generate_random",
171    "list_tasks",
172    "execute_sql",
173    "list_databases",
174];
175
176/// Whether `name` is a tool — see [`TOOL_ACTION_NAMES`].
177pub fn is_tool_name(name: &str) -> bool {
178    TOOL_ACTION_NAMES.contains(&name)
179}
180
181impl ToolAction {
182    /// Merge protocols array with optional single protocol (for backwards compatibility)
183    fn merge_protocols(protocols: &[String], protocol: &Option<String>) -> Vec<String> {
184        let mut result = protocols.to_vec();
185        if let Some(p) = protocol {
186            if !result.contains(p) {
187                result.push(p.clone());
188            }
189        }
190        result
191    }
192
193    /// Parse from JSON value
194    pub fn from_json(value: &serde_json::Value) -> Result<Self> {
195        // Check if the tool type is recognized first
196        if let Some(action_type) = value.get("type").and_then(|t| t.as_str()) {
197            if !is_tool_name(action_type) {
198                anyhow::bail!(
199                    "Unknown tool type: '{}'. Valid tools: {}",
200                    action_type,
201                    TOOL_ACTION_NAMES.join(", ")
202                );
203            }
204        }
205
206        serde_json::from_value(value.clone()).context("Malformed tool action")
207    }
208
209    /// Check if a JSON value is a tool action
210    pub fn is_tool_action(value: &serde_json::Value) -> bool {
211        value
212            .get("type")
213            .and_then(|t| t.as_str())
214            .is_some_and(is_tool_name)
215    }
216
217    /// Get a brief description of this tool action for logging
218    pub fn describe(&self) -> String {
219        match self {
220            ToolAction::ReadFile {
221                path,
222                mode,
223                lines,
224                pattern,
225                ..
226            } => {
227                let mut desc = format!("read_file: {}", path);
228                match mode.as_str() {
229                    "head" => desc.push_str(&format!(
230                        " (head, {} lines)",
231                        lines.unwrap_or(DEFAULT_LINES)
232                    )),
233                    "tail" => desc.push_str(&format!(
234                        " (tail, {} lines)",
235                        lines.unwrap_or(DEFAULT_LINES)
236                    )),
237                    "grep" => {
238                        if let Some(p) = pattern {
239                            desc.push_str(&format!(" (grep: {})", p));
240                        }
241                    }
242                    _ => desc.push_str(" (full)"),
243                }
244                desc
245            }
246            ToolAction::WebSearch { query } => {
247                format!("web_search: \"{}\"", query)
248            }
249            ToolAction::ReadBaseStackDocs { protocol } => {
250                format!("read_base_stack_docs: \"{}\"", protocol)
251            }
252            ToolAction::ReadServerDocumentation {
253                protocols,
254                protocol,
255            } => {
256                let all_protocols = Self::merge_protocols(protocols, protocol);
257                format!("read_server_documentation: {:?}", all_protocols)
258            }
259            ToolAction::ReadClientDocumentation {
260                protocols,
261                protocol,
262            } => {
263                let all_protocols = Self::merge_protocols(protocols, protocol);
264                format!("read_client_documentation: {:?}", all_protocols)
265            }
266            ToolAction::ReadDocumentation {
267                protocols,
268                protocol,
269            } => {
270                let all_protocols = Self::merge_protocols(protocols, protocol);
271                format!("read_documentation: {:?}", all_protocols)
272            }
273            ToolAction::ListModels => "list_models: query available Ollama models".to_string(),
274            ToolAction::GenerateRandom {
275                data_type,
276                length,
277                min,
278                max,
279                ..
280            } => {
281                let mut desc = format!("generate_random: {}", data_type);
282                if let Some(len) = length {
283                    desc.push_str(&format!(" (length: {})", len));
284                }
285                if let Some(min_val) = min {
286                    desc.push_str(&format!(" (min: {})", min_val));
287                }
288                if let Some(max_val) = max {
289                    desc.push_str(&format!(" (max: {})", max_val));
290                }
291                desc
292            }
293            ToolAction::ListTasks => "list_tasks: list all scheduled tasks".to_string(),
294            #[cfg(feature = "sqlite")]
295            ToolAction::ExecuteSql { database_id, query } => {
296                format!("execute_sql: db-{} query=\"{}\"", database_id, query)
297            }
298            #[cfg(feature = "sqlite")]
299            ToolAction::ListDatabases => "list_databases: list all databases".to_string(),
300        }
301    }
302}
303
304/// Result of executing a tool
305#[derive(Debug, Clone, Serialize)]
306pub struct ToolResult {
307    /// Name of the tool that was executed
308    pub tool: String,
309
310    /// Brief description of what was executed
311    pub description: String,
312
313    /// Result of the tool execution (success or error message)
314    pub result: String,
315
316    /// Whether the tool execution was successful
317    pub success: bool,
318
319    /// Number of lines/items in result (for logging)
320    pub result_size: usize,
321}
322
323impl ToolResult {
324    /// Create a successful tool result
325    pub fn success(
326        tool: impl Into<String>,
327        description: impl Into<String>,
328        result: impl Into<String>,
329    ) -> Self {
330        let result_str = result.into();
331        let result_size = result_str.lines().count();
332        Self {
333            tool: tool.into(),
334            description: description.into(),
335            result: result_str,
336            success: true,
337            result_size,
338        }
339    }
340
341    /// Create a failed tool result
342    pub fn error(
343        tool: impl Into<String>,
344        description: impl Into<String>,
345        error: impl Into<String>,
346    ) -> Self {
347        Self {
348            tool: tool.into(),
349            description: description.into(),
350            result: error.into(),
351            success: false,
352            result_size: 0,
353        }
354    }
355
356    /// Format for inclusion in LLM prompt
357    pub fn to_prompt_text(&self) -> String {
358        if self.success {
359            format!(
360                "Tool '{}' ({}) returned {} lines:\n{}",
361                self.tool, self.description, self.result_size, self.result
362            )
363        } else {
364            format!(
365                "Tool '{}' ({}) error:\n{}",
366                self.tool, self.description, self.result
367            )
368        }
369    }
370
371    /// Get a brief summary for logging
372    pub fn summary(&self) -> String {
373        if self.success {
374            format!("{} ({} lines)", self.description, self.result_size)
375        } else {
376            format!("{} (error)", self.description)
377        }
378    }
379}
380
381/// Execute a read_file tool action
382pub async fn execute_read_file(
383    path: &str,
384    mode: &str,
385    lines: Option<usize>,
386    pattern: Option<&str>,
387    context_before: Option<usize>,
388    context_after: Option<usize>,
389) -> ToolResult {
390    use tracing::info;
391
392    info!("🔧 Tool: read_file - path={}, mode={}", path, mode);
393    debug!("Executing read_file tool: path={}, mode={}", path, mode);
394
395    // Resolve path (support both relative and absolute)
396    let path_buf = PathBuf::from(path);
397    let resolved_path = if path_buf.is_absolute() {
398        path_buf
399    } else {
400        // Resolve relative to current working directory
401        match std::env::current_dir() {
402            Ok(cwd) => cwd.join(&path_buf),
403            Err(e) => {
404                error!("Failed to get current working directory: {}", e);
405                return ToolResult::error(
406                    "read_file",
407                    format!("{} ({})", path, mode),
408                    format!("Failed to resolve path: {}", e),
409                );
410            }
411        }
412    };
413
414    // Security check: ensure path exists and is a file
415    if !resolved_path.exists() {
416        warn!("File not found: {}", resolved_path.display());
417        info!("  ✗ File not found: {}", path);
418        return ToolResult::error(
419            "read_file",
420            format!("{} ({})", path, mode),
421            format!("File not found: {}", path),
422        );
423    }
424
425    if !resolved_path.is_file() {
426        warn!("Path is not a file: {}", resolved_path.display());
427        info!("  ✗ Path is not a file: {}", path);
428        return ToolResult::error(
429            "read_file",
430            format!("{} ({})", path, mode),
431            format!("Path is not a file: {}", path),
432        );
433    }
434
435    // Check file size
436    match std::fs::metadata(&resolved_path) {
437        Ok(metadata) => {
438            if metadata.len() > MAX_FILE_SIZE {
439                warn!("File too large: {} bytes", metadata.len());
440                info!(
441                    "  ✗ File too large: {} bytes (max: {} bytes)",
442                    metadata.len(),
443                    MAX_FILE_SIZE
444                );
445                return ToolResult::error(
446                    "read_file",
447                    format!("{} ({})", path, mode),
448                    format!(
449                        "File too large: {} bytes (max: {} bytes)",
450                        metadata.len(),
451                        MAX_FILE_SIZE
452                    ),
453                );
454            }
455        }
456        Err(e) => {
457            error!("Failed to read file metadata: {}", e);
458            info!("  ✗ Failed to read file metadata: {}", e);
459            return ToolResult::error(
460                "read_file",
461                format!("{} ({})", path, mode),
462                format!("Failed to read file metadata: {}", e),
463            );
464        }
465    }
466
467    // Read file contents
468    let contents = match crate::utils::file_io::read_text_async(
469        &resolved_path,
470        MAX_FILE_SIZE as usize,
471    )
472    .await
473    {
474        Ok(c) => c,
475        Err(e) => {
476            error!("Failed to read file: {}", e);
477            info!("  ✗ Failed to read file: {}", e);
478            return ToolResult::error(
479                "read_file",
480                format!("{} ({})", path, mode),
481                format!("Failed to read file: {}", e),
482            );
483        }
484    };
485
486    // Process based on mode
487    let result = match mode {
488        "full" => {
489            debug!("Read full file: {} bytes", contents.len());
490            info!("  ✓ Read full file: {} bytes", contents.len());
491            contents
492        }
493        "head" => {
494            let n = lines.unwrap_or(DEFAULT_LINES);
495            let result: Vec<&str> = contents.lines().take(n).collect();
496            debug!("Read head: {} lines", result.len());
497            info!("  ✓ Read head: {} lines", result.len());
498            result.join("\n")
499        }
500        "tail" => {
501            let n = lines.unwrap_or(DEFAULT_LINES);
502            let all_lines: Vec<&str> = contents.lines().collect();
503            let start = all_lines.len().saturating_sub(n);
504            let result = &all_lines[start..];
505            debug!("Read tail: {} lines", result.len());
506            info!("  ✓ Read tail: {} lines", result.len());
507            result.join("\n")
508        }
509        "grep" => {
510            if let Some(pat) = pattern {
511                match grep_with_context(&contents, pat, context_before, context_after) {
512                    Ok(result) => {
513                        let line_count = result.lines().count();
514                        debug!("Grep found {} matching lines", line_count);
515                        info!("  ✓ Grep found {} matching lines", line_count);
516                        result
517                    }
518                    Err(e) => {
519                        error!("Grep failed: {}", e);
520                        info!("  ✗ Grep failed: {}", e);
521                        return ToolResult::error(
522                            "read_file",
523                            format!("{} (grep: {})", path, pat),
524                            format!("Grep failed: {}", e),
525                        );
526                    }
527                }
528            } else {
529                return ToolResult::error(
530                    "read_file",
531                    format!("{} (grep)", path),
532                    "Grep mode requires 'pattern' parameter".to_string(),
533                );
534            }
535        }
536        _ => {
537            return ToolResult::error(
538                "read_file",
539                format!("{} ({})", path, mode),
540                format!("Invalid mode: '{}'. Use: full, head, tail, or grep", mode),
541            );
542        }
543    };
544
545    let description = match mode {
546        "full" => format!("{} (full)", path),
547        "head" => format!("{} (head, {} lines)", path, lines.unwrap_or(DEFAULT_LINES)),
548        "tail" => format!("{} (tail, {} lines)", path, lines.unwrap_or(DEFAULT_LINES)),
549        "grep" => format!("{} (grep: {})", path, pattern.unwrap_or("")),
550        _ => format!("{} ({})", path, mode),
551    };
552
553    ToolResult::success("read_file", description, result)
554}
555
556/// Perform grep with context lines
557fn grep_with_context(
558    text: &str,
559    pattern: &str,
560    before: Option<usize>,
561    after: Option<usize>,
562) -> Result<String> {
563    let regex = Regex::new(pattern).context("Invalid regex pattern")?;
564    let lines: Vec<&str> = text.lines().collect();
565    let before_lines = before.unwrap_or(0);
566    let after_lines = after.unwrap_or(0);
567
568    let mut result_lines = Vec::new();
569    let mut matched_indices = std::collections::HashSet::new();
570
571    // Find all matching lines and their context
572    for (i, line) in lines.iter().enumerate() {
573        if regex.is_match(line) {
574            // Add context before
575            let start = i.saturating_sub(before_lines);
576            for idx in start..i {
577                matched_indices.insert(idx);
578            }
579
580            // Add matching line
581            matched_indices.insert(i);
582
583            // Add context after
584            let end = (i + after_lines + 1).min(lines.len());
585            for idx in (i + 1)..end {
586                matched_indices.insert(idx);
587            }
588        }
589    }
590
591    // Convert to sorted vector and build result
592    let mut indices: Vec<usize> = matched_indices.into_iter().collect();
593    indices.sort_unstable();
594
595    for idx in indices {
596        result_lines.push(lines[idx]);
597    }
598
599    Ok(result_lines.join("\n"))
600}
601
602/// Execute a web_search tool action
603pub async fn execute_web_search(query: &str) -> ToolResult {
604    #[cfg(target_arch = "wasm32")]
605    {
606        return ToolResult::error(
607            "web_search",
608            query.to_string(),
609            "web search is not available in the browser build".to_string(),
610        );
611    }
612    #[cfg(not(target_arch = "wasm32"))]
613    {
614        use tracing::info;
615
616        info!("🔧 Tool: web_search - query=\"{}\"", query);
617        debug!("Executing web_search tool for query: {}", query);
618
619        // Check if query is a URL - if so, fetch it directly
620        if query.trim().starts_with("http://") || query.trim().starts_with("https://") {
621            return fetch_url(query.trim()).await;
622        }
623
624        // Otherwise, use DuckDuckGo HTML search (no API key required)
625        use url::form_urlencoded;
626        let encoded_query = form_urlencoded::byte_serialize(query.as_bytes()).collect::<String>();
627        let url = format!("https://html.duckduckgo.com/html/?q={}", encoded_query);
628
629        let client = {
630            let builder =
631                reqwest::Client::builder().user_agent("Mozilla/5.0 (compatible; NetGet/1.0)");
632            // fetch() has no request timeout; reqwest's wasm builder has no method for one.
633            #[cfg(not(target_arch = "wasm32"))]
634            let builder = builder.timeout(std::time::Duration::from_secs(10));
635            builder.build().unwrap_or_default()
636        };
637
638        match client.get(&url).send().await {
639            Ok(response) => {
640                if !response.status().is_success() {
641                    warn!("Web search failed with status: {}", response.status());
642                    info!("  ✗ Search failed with status: {}", response.status());
643                    return ToolResult::error(
644                        "web_search",
645                        query.to_string(),
646                        format!("Search failed with status: {}", response.status()),
647                    );
648                }
649
650                match crate::client::http_fetch::read_response_text(response, 8 * 1024 * 1024).await
651                {
652                    Ok(html) => {
653                        // Parse search results from HTML
654                        let results = parse_duckduckgo_results(&html);
655                        if results.is_empty() {
656                            info!("  ⚠ No results found");
657                            ToolResult::success(
658                                "web_search",
659                                query.to_string(),
660                                "No results found.",
661                            )
662                        } else {
663                            debug!("Found {} search results", results.len());
664                            info!("  ✓ Found {} search results", results.len());
665                            let formatted = format_search_results(&results);
666                            ToolResult::success("web_search", query.to_string(), formatted)
667                        }
668                    }
669                    Err(e) => {
670                        error!("Failed to read search response: {}", e);
671                        info!("  ✗ Failed to read response: {}", e);
672                        ToolResult::error(
673                            "web_search",
674                            query.to_string(),
675                            format!("Failed to read response: {}", e),
676                        )
677                    }
678                }
679            }
680            Err(e) => {
681                error!("Web search request failed: {}", e);
682                info!("  ✗ Request failed: {}", e);
683                ToolResult::error(
684                    "web_search",
685                    query.to_string(),
686                    format!("Request failed: {}", e),
687                )
688            }
689        }
690    }
691}
692
693/// Fetch a URL directly and convert HTML to text
694async fn fetch_url(url: &str) -> ToolResult {
695    #[cfg(target_arch = "wasm32")]
696    {
697        return ToolResult::error(
698            "web_search",
699            url.to_string(),
700            "fetching URLs is not available in the browser build".to_string(),
701        );
702    }
703    #[cfg(not(target_arch = "wasm32"))]
704    {
705        use tracing::info;
706
707        info!("🔧 Tool: web_search (fetch URL) - {}", url);
708        debug!("Fetching URL directly: {}", url);
709
710        let client = {
711            let builder =
712                reqwest::Client::builder().user_agent("Mozilla/5.0 (compatible; NetGet/1.0)");
713            // fetch() has no request timeout; reqwest's wasm builder has no method for one.
714            #[cfg(not(target_arch = "wasm32"))]
715            let builder = builder.timeout(std::time::Duration::from_secs(15));
716            builder.build().unwrap_or_default()
717        };
718
719        match client.get(url).send().await {
720            Ok(response) => {
721                if !response.status().is_success() {
722                    warn!("URL fetch failed with status: {}", response.status());
723                    info!("  ✗ HTTP {}", response.status());
724                    return ToolResult::error(
725                        "web_search",
726                        url.to_string(),
727                        format!("Failed to fetch URL: HTTP {}", response.status()),
728                    );
729                }
730
731                match crate::client::http_fetch::read_response_text(response, 8 * 1024 * 1024).await
732                {
733                    Ok(html) => {
734                        // Convert HTML to plain text
735                        let text = html2text::from_read(html.as_bytes(), 120);
736
737                        if text.trim().is_empty() {
738                            info!("  ⚠ URL fetched but no text content found");
739                            ToolResult::success(
740                                "web_search",
741                                url.to_string(),
742                                "URL fetched but no text content found.",
743                            )
744                        } else {
745                            // Truncate to reasonable length (10000 chars)
746                            let truncated = crate::utils::truncate_with_notice(&text, 10000);
747
748                            debug!("Fetched URL: {} chars", truncated.len());
749                            info!("  ✓ Fetched URL: {} chars", truncated.len());
750                            ToolResult::success("web_search", url.to_string(), truncated)
751                        }
752                    }
753                    Err(e) => {
754                        error!("Failed to read URL response: {}", e);
755                        info!("  ✗ Failed to read response: {}", e);
756                        ToolResult::error(
757                            "web_search",
758                            url.to_string(),
759                            format!("Failed to read response: {}", e),
760                        )
761                    }
762                }
763            }
764            Err(e) => {
765                error!("URL fetch request failed: {}", e);
766                info!("  ✗ Request failed: {}", e);
767                ToolResult::error(
768                    "web_search",
769                    url.to_string(),
770                    format!("Request failed: {}", e),
771                )
772            }
773        }
774    }
775}
776
777/// Search result entry
778#[derive(Debug, Clone)]
779struct SearchResult {
780    title: String,
781    url: String,
782    snippet: String,
783}
784
785/// Parse DuckDuckGo HTML results
786fn parse_duckduckgo_results(html: &str) -> Vec<SearchResult> {
787    let mut results = Vec::new();
788
789    // Simple HTML parsing - look for result blocks
790    // DuckDuckGo HTML has results in <div class="result"> blocks
791    for result_block in html.split(r#"<div class="result"#) {
792        // Extract title from <a class="result__a">
793        let title = extract_between(result_block, r#"class="result__a""#, "</a>")
794            .and_then(|s| extract_between(&s, ">", "<"))
795            .map(|s| decode_html_entities(&s))
796            .unwrap_or_default();
797
798        // Extract URL from href
799        let url = extract_between(result_block, r#"href="//"#, r#"""#)
800            .map(|s| format!("https://{}", s))
801            .or_else(|| {
802                extract_between(result_block, r#"href="https://"#, r#"""#)
803                    .map(|s| format!("https://{}", s))
804            })
805            .unwrap_or_default();
806
807        // Extract snippet from <a class="result__snippet">
808        let snippet = extract_between(result_block, r#"class="result__snippet""#, "</a>")
809            .and_then(|s| extract_between(&s, ">", "<"))
810            .map(|s| decode_html_entities(&s))
811            .unwrap_or_default();
812
813        if !title.is_empty() || !snippet.is_empty() {
814            results.push(SearchResult {
815                title,
816                url,
817                snippet,
818            });
819        }
820
821        // Limit to top 5 results
822        if results.len() >= 5 {
823            break;
824        }
825    }
826
827    results
828}
829
830/// Extract text between two delimiters
831fn extract_between(text: &str, start: &str, end: &str) -> Option<String> {
832    let start_idx = text.find(start)? + start.len();
833    let remaining = &text[start_idx..];
834    let end_idx = remaining.find(end)?;
835    Some(remaining[..end_idx].to_string())
836}
837
838/// Decode common HTML entities
839fn decode_html_entities(text: &str) -> String {
840    text.replace("&amp;", "&")
841        .replace("&lt;", "<")
842        .replace("&gt;", ">")
843        .replace("&quot;", "\"")
844        .replace("&#39;", "'")
845        .replace("&nbsp;", " ")
846}
847
848/// Format search results for LLM
849fn format_search_results(results: &[SearchResult]) -> String {
850    let mut formatted = String::from("Search results:\n\n");
851    for (i, result) in results.iter().enumerate() {
852        formatted.push_str(&format!("{}. {}\n", i + 1, result.title));
853        if !result.url.is_empty() {
854            formatted.push_str(&format!("   URL: {}\n", result.url));
855        }
856        if !result.snippet.is_empty() {
857            formatted.push_str(&format!("   {}\n", result.snippet));
858        }
859        formatted.push('\n');
860    }
861    formatted
862}
863
864/// Execute a list_models tool action
865pub async fn execute_list_models() -> ToolResult {
866    #[cfg(target_arch = "wasm32")]
867    {
868        return ToolResult::error(
869            "list_models",
870            "query available models".to_string(),
871            "the browser build has no Ollama to list models from".to_string(),
872        );
873    }
874    #[cfg(not(target_arch = "wasm32"))]
875    {
876        use tracing::info;
877
878        info!("🔧 Tool: list_models - querying Ollama for available models");
879        debug!("Executing list_models tool");
880
881        // Create Ollama client
882        let client = crate::llm::ollama_client::OllamaClient::new("http://localhost:11434");
883
884        match client.list_models().await {
885            Ok(models) => {
886                if models.is_empty() {
887                    info!("  ⚠ No models found in Ollama");
888                    ToolResult::success(
889                        "list_models",
890                        "query available models".to_string(),
891                        "No models found. Please pull a model using 'ollama pull <model-name>'.",
892                    )
893                } else {
894                    let model_count = models.len();
895                    let formatted = format!(
896                        "Available Ollama models ({} total):\n\n{}\n\nYou can use any of these models with the change_model action.",
897                        model_count,
898                        models.join("\n")
899                    );
900                    debug!("Found {} models", model_count);
901                    info!("  ✓ Found {} models", model_count);
902                    ToolResult::success(
903                        "list_models",
904                        "query available models".to_string(),
905                        formatted,
906                    )
907                }
908            }
909            Err(e) => {
910                error!("Failed to list models: {}", e);
911                info!("  ✗ Failed to list models: {}", e);
912                ToolResult::error(
913                    "list_models",
914                    "query available models".to_string(),
915                    format!("Failed to list models: {}. Is Ollama running?", e),
916                )
917            }
918        }
919    }
920}
921
922/// Execute a generate_random tool action
923pub async fn execute_generate_random(
924    data_type: &str,
925    length: Option<usize>,
926    min: Option<f64>,
927    max: Option<f64>,
928    charset: Option<&str>,
929    choices: Option<&[serde_json::Value]>,
930    count: Option<usize>,
931) -> ToolResult {
932    use rand::Rng;
933    use tracing::info;
934
935    info!("🔧 Tool: generate_random - type={}", data_type);
936    debug!("Generating random data: type={}", data_type);
937
938    let mut rng = rand::thread_rng();
939
940    let result = match data_type {
941        // UUID v4
942        "uuid" | "uuid4" => {
943            let uuid = uuid::Uuid::new_v4();
944            info!("  ✓ Generated UUID: {}", uuid);
945            uuid.to_string()
946        }
947
948        // Random integer
949        "integer" | "int" => {
950            let min_val = min.unwrap_or(0.0) as i64;
951            let max_val = max.unwrap_or(100.0) as i64;
952            if min_val >= max_val {
953                return ToolResult::error(
954                    "generate_random",
955                    format!("{} (int)", data_type),
956                    "min must be less than max".to_string(),
957                );
958            }
959            let value = rng.gen_range(min_val..=max_val);
960            info!("  ✓ Generated integer: {}", value);
961            value.to_string()
962        }
963
964        // Random float
965        "float" | "number" => {
966            let min_val = min.unwrap_or(0.0);
967            let max_val = max.unwrap_or(1.0);
968            if min_val >= max_val {
969                return ToolResult::error(
970                    "generate_random",
971                    format!("{} (float)", data_type),
972                    "min must be less than max".to_string(),
973                );
974            }
975            let value: f64 = rng.gen_range(min_val..=max_val);
976            info!("  ✓ Generated float: {}", value);
977            value.to_string()
978        }
979
980        // Random string
981        "string" => {
982            let len = length.unwrap_or(16);
983            let chars: Vec<char> = match charset.unwrap_or("alphanumeric") {
984                "hex" => "0123456789abcdef",
985                "digits" => "0123456789",
986                "letters" => "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
987                "lowercase" => "abcdefghijklmnopqrstuvwxyz",
988                "uppercase" => "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
989                _ => "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
990            }
991            .chars()
992            .collect();
993
994            let result: String = (0..len)
995                .map(|_| {
996                    let idx = rng.gen_range(0..chars.len());
997                    chars[idx]
998                })
999                .collect();
1000            info!("  ✓ Generated string: {} chars", len);
1001            result
1002        }
1003
1004        // Random hex bytes (as hex string)
1005        "hex" | "hex_bytes" => {
1006            let len = length.unwrap_or(16);
1007            let bytes: Vec<u8> = (0..len).map(|_| rng.gen()).collect();
1008            let hex_string = bytes
1009                .iter()
1010                .map(|b| format!("{:02x}", b))
1011                .collect::<String>();
1012            info!("  ✓ Generated hex bytes: {} bytes", len);
1013            hex_string
1014        }
1015
1016        // Random base64 string
1017        "base64" => {
1018            use base64::{engine::general_purpose, Engine as _};
1019            let len = length.unwrap_or(16);
1020            let bytes: Vec<u8> = (0..len).map(|_| rng.gen()).collect();
1021            let b64 = general_purpose::STANDARD.encode(&bytes);
1022            info!("  ✓ Generated base64: {} bytes", len);
1023            b64
1024        }
1025
1026        // Random word (lorem ipsum style)
1027        "word" => {
1028            const WORDS: &[&str] = &[
1029                "lorem",
1030                "ipsum",
1031                "dolor",
1032                "sit",
1033                "amet",
1034                "consectetur",
1035                "adipiscing",
1036                "elit",
1037                "sed",
1038                "do",
1039                "eiusmod",
1040                "tempor",
1041                "incididunt",
1042                "ut",
1043                "labore",
1044                "et",
1045                "dolore",
1046                "magna",
1047                "aliqua",
1048                "enim",
1049                "ad",
1050                "minim",
1051                "veniam",
1052                "quis",
1053                "nostrud",
1054                "exercitation",
1055                "ullamco",
1056                "laboris",
1057                "nisi",
1058                "aliquip",
1059                "ex",
1060                "ea",
1061                "commodo",
1062                "consequat",
1063                "duis",
1064                "aute",
1065                "irure",
1066                "in",
1067                "reprehenderit",
1068                "voluptate",
1069                "velit",
1070                "esse",
1071                "cillum",
1072                "fugiat",
1073                "nulla",
1074                "pariatur",
1075                "excepteur",
1076                "sint",
1077                "occaecat",
1078                "cupidatat",
1079                "non",
1080                "proident",
1081                "sunt",
1082                "culpa",
1083                "qui",
1084                "officia",
1085                "deserunt",
1086                "mollit",
1087                "anim",
1088                "id",
1089                "est",
1090                "laborum",
1091            ];
1092            let word = WORDS[rng.gen_range(0..WORDS.len())];
1093            info!("  ✓ Generated word: {}", word);
1094            word.to_string()
1095        }
1096
1097        // Random sentence
1098        "sentence" => {
1099            const WORDS: &[&str] = &[
1100                "lorem",
1101                "ipsum",
1102                "dolor",
1103                "sit",
1104                "amet",
1105                "consectetur",
1106                "adipiscing",
1107                "elit",
1108                "sed",
1109                "do",
1110                "eiusmod",
1111                "tempor",
1112                "incididunt",
1113                "ut",
1114                "labore",
1115                "et",
1116                "dolore",
1117                "magna",
1118                "aliqua",
1119            ];
1120            let word_count = length.unwrap_or(10);
1121            let sentence: Vec<String> = (0..word_count)
1122                .map(|i| {
1123                    let word = WORDS[rng.gen_range(0..WORDS.len())];
1124                    if i == 0 {
1125                        let mut s = word.to_string();
1126                        s[0..1].make_ascii_uppercase();
1127                        s
1128                    } else {
1129                        word.to_string()
1130                    }
1131                })
1132                .collect();
1133            let result = format!("{}.", sentence.join(" "));
1134            info!("  ✓ Generated sentence: {} words", word_count);
1135            result
1136        }
1137
1138        // Random paragraph
1139        "paragraph" => {
1140            const WORDS: &[&str] = &[
1141                "lorem",
1142                "ipsum",
1143                "dolor",
1144                "sit",
1145                "amet",
1146                "consectetur",
1147                "adipiscing",
1148                "elit",
1149                "sed",
1150                "do",
1151                "eiusmod",
1152                "tempor",
1153                "incididunt",
1154                "ut",
1155                "labore",
1156                "et",
1157                "dolore",
1158                "magna",
1159                "aliqua",
1160                "enim",
1161                "ad",
1162                "minim",
1163                "veniam",
1164                "quis",
1165                "nostrud",
1166            ];
1167            let sentence_count = length.unwrap_or(5);
1168            let paragraph: Vec<String> = (0..sentence_count)
1169                .map(|_| {
1170                    let word_count = rng.gen_range(5..15);
1171                    let sentence_words: Vec<String> = (0..word_count)
1172                        .map(|i| {
1173                            let word = WORDS[rng.gen_range(0..WORDS.len())];
1174                            if i == 0 {
1175                                let mut s = word.to_string();
1176                                s[0..1].make_ascii_uppercase();
1177                                s
1178                            } else {
1179                                word.to_string()
1180                            }
1181                        })
1182                        .collect();
1183                    format!("{}.", sentence_words.join(" "))
1184                })
1185                .collect();
1186            info!("  ✓ Generated paragraph: {} sentences", sentence_count);
1187            paragraph.join(" ")
1188        }
1189
1190        // Random email
1191        "email" => {
1192            let username_len = length.unwrap_or(8);
1193            let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz0123456789".chars().collect();
1194            let username: String = (0..username_len)
1195                .map(|_| {
1196                    let idx = rng.gen_range(0..chars.len());
1197                    chars[idx]
1198                })
1199                .collect();
1200            let domains = &["example.com", "test.com", "demo.org", "sample.net"];
1201            let domain = domains[rng.gen_range(0..domains.len())];
1202            let email = format!("{}@{}", username, domain);
1203            info!("  ✓ Generated email: {}", email);
1204            email
1205        }
1206
1207        // Random IPv4 address
1208        "ipv4" | "ip" => {
1209            let ip = format!(
1210                "{}.{}.{}.{}",
1211                rng.gen_range(1..=255),
1212                rng.gen_range(0..=255),
1213                rng.gen_range(0..=255),
1214                rng.gen_range(1..=255)
1215            );
1216            info!("  ✓ Generated IPv4: {}", ip);
1217            ip
1218        }
1219
1220        // Random IPv6 address
1221        "ipv6" => {
1222            let segments: Vec<String> = (0..8)
1223                .map(|_| format!("{:04x}", rng.gen_range(0..=0xffff)))
1224                .collect();
1225            let ip = segments.join(":");
1226            info!("  ✓ Generated IPv6: {}", ip);
1227            ip
1228        }
1229
1230        // Random MAC address
1231        "mac" | "mac_address" => {
1232            let octets: Vec<String> = (0..6)
1233                .map(|_| format!("{:02x}", rng.gen_range(0..=255)))
1234                .collect();
1235            let mac = octets.join(":");
1236            info!("  ✓ Generated MAC address: {}", mac);
1237            mac
1238        }
1239
1240        // Random port number
1241        "port" => {
1242            let min_port = min.unwrap_or(1024.0) as u16;
1243            let max_port = max.unwrap_or(65535.0) as u16;
1244            let port = rng.gen_range(min_port..=max_port);
1245            info!("  ✓ Generated port: {}", port);
1246            port.to_string()
1247        }
1248
1249        // Random timestamp (Unix timestamp)
1250        "timestamp" | "unix_timestamp" => {
1251            use crate::utils::clock::{SystemTime, UNIX_EPOCH};
1252            let now = SystemTime::now()
1253                .duration_since(UNIX_EPOCH)
1254                .map(|d| d.as_secs())
1255                .unwrap_or(0);
1256            // Generate random timestamp within ±1 year from now
1257            let one_year = 365 * 24 * 60 * 60;
1258            let min_time = min
1259                .map(|m| m as u64)
1260                .unwrap_or(now.saturating_sub(one_year));
1261            let max_time = max
1262                .map(|m| m as u64)
1263                .unwrap_or(now.saturating_add(one_year));
1264            let timestamp = rng.gen_range(min_time..=max_time);
1265            info!("  ✓ Generated timestamp: {}", timestamp);
1266            timestamp.to_string()
1267        }
1268
1269        // Random date (ISO 8601 format)
1270        "date" | "iso_date" => {
1271            use crate::utils::clock::{SystemTime, UNIX_EPOCH};
1272            let now = SystemTime::now()
1273                .duration_since(UNIX_EPOCH)
1274                .map(|d| d.as_secs())
1275                .unwrap_or(0);
1276            let one_year = 365 * 24 * 60 * 60;
1277            let min_time = min
1278                .map(|m| m as u64)
1279                .unwrap_or(now.saturating_sub(one_year));
1280            let max_time = max.map(|m| m as u64).unwrap_or(now + one_year);
1281            let timestamp = rng.gen_range(min_time..=max_time);
1282
1283            // Convert to date
1284            let days_since_epoch = timestamp / (24 * 60 * 60);
1285            let year = 1970 + (days_since_epoch / 365);
1286            let day_of_year = days_since_epoch % 365;
1287            let month = (day_of_year / 30) + 1;
1288            let day = (day_of_year % 30) + 1;
1289
1290            let date = format!("{:04}-{:02}-{:02}", year, month.min(12), day.min(28));
1291            info!("  ✓ Generated date: {}", date);
1292            date
1293        }
1294
1295        // Random boolean
1296        "boolean" | "bool" => {
1297            let value = rng.gen_bool(0.5);
1298            info!("  ✓ Generated boolean: {}", value);
1299            value.to_string()
1300        }
1301
1302        // Pick random choice from list
1303        "choice" => {
1304            if let Some(choice_list) = choices {
1305                if choice_list.is_empty() {
1306                    return ToolResult::error(
1307                        "generate_random",
1308                        "choice".to_string(),
1309                        "choices array cannot be empty".to_string(),
1310                    );
1311                }
1312                let idx = rng.gen_range(0..choice_list.len());
1313                let choice = &choice_list[idx];
1314                info!("  ✓ Picked random choice: {}", choice);
1315                serde_json::to_string(choice).unwrap_or_else(|_| choice.to_string())
1316            } else {
1317                return ToolResult::error(
1318                    "generate_random",
1319                    "choice".to_string(),
1320                    "choices parameter is required for choice type".to_string(),
1321                );
1322            }
1323        }
1324
1325        // Pick multiple random choices from list
1326        "choices" | "sample" => {
1327            if let Some(choice_list) = choices {
1328                if choice_list.is_empty() {
1329                    return ToolResult::error(
1330                        "generate_random",
1331                        "choices".to_string(),
1332                        "choices array cannot be empty".to_string(),
1333                    );
1334                }
1335                let pick_count = count.unwrap_or(1).min(choice_list.len());
1336                let mut indices: Vec<usize> = (0..choice_list.len()).collect();
1337                use rand::seq::SliceRandom;
1338                indices.shuffle(&mut rng);
1339
1340                let picked: Vec<String> = indices[..pick_count]
1341                    .iter()
1342                    .map(|&i| {
1343                        serde_json::to_string(&choice_list[i])
1344                            .unwrap_or_else(|_| choice_list[i].to_string())
1345                    })
1346                    .collect();
1347
1348                info!("  ✓ Picked {} random choices", pick_count);
1349                format!("[{}]", picked.join(", "))
1350            } else {
1351                return ToolResult::error(
1352                    "generate_random",
1353                    "choices".to_string(),
1354                    "choices parameter is required for choices type".to_string(),
1355                );
1356            }
1357        }
1358
1359        _ => {
1360            return ToolResult::error(
1361                "generate_random",
1362                data_type.to_string(),
1363                format!(
1364                    "Unknown data type: '{}'. Valid types: uuid, integer, float, string, hex, base64, word, sentence, paragraph, email, ipv4, ipv6, mac, port, timestamp, date, boolean, choice, choices",
1365                    data_type
1366                ),
1367            );
1368        }
1369    };
1370
1371    ToolResult::success("generate_random", data_type.to_string(), result)
1372}
1373
1374/// Get action definition for read_file tool
1375pub fn read_file_action() -> ActionDefinition {
1376    ActionDefinition {
1377        name: "read_file".to_string(),
1378        description: "Read the contents of a file from the local filesystem. Supports multiple read modes: full (entire file), head (first N lines), tail (last N lines), or grep (search with regex pattern). Use this to access configuration files, schemas, RFCs, or other reference documents.".to_string(),
1379        parameters: vec![
1380            Parameter {
1381                name: "path".to_string(),
1382                type_hint: "string".to_string(),
1383                description: "Path to the file (relative to current directory or absolute)".to_string(),
1384                required: true,
1385            },
1386            Parameter {
1387                name: "mode".to_string(),
1388                type_hint: "string".to_string(),
1389                description: "Read mode: 'full' (default), 'head', 'tail', or 'grep'".to_string(),
1390                required: false,
1391            },
1392            Parameter {
1393                name: "lines".to_string(),
1394                type_hint: "number".to_string(),
1395                description: "Number of lines for head/tail mode (default: 50)".to_string(),
1396                required: false,
1397            },
1398            Parameter {
1399                name: "pattern".to_string(),
1400                type_hint: "string".to_string(),
1401                description: "Regex pattern for grep mode (required for grep)".to_string(),
1402                required: false,
1403            },
1404            Parameter {
1405                name: "context_before".to_string(),
1406                type_hint: "number".to_string(),
1407                description: "Lines of context before match in grep mode (like grep -B)".to_string(),
1408                required: false,
1409            },
1410            Parameter {
1411                name: "context_after".to_string(),
1412                type_hint: "number".to_string(),
1413                description: "Lines of context after match in grep mode (like grep -A)".to_string(),
1414                required: false,
1415            },
1416        ],
1417        example: json!({
1418            "type": "read_file",
1419            "path": "schema.json",
1420            "mode": "full"
1421        }),
1422        log_template: None,
1423    }
1424}
1425
1426/// Get action definition for web_search tool
1427pub fn web_search_action() -> ActionDefinition {
1428    ActionDefinition {
1429        name: "web_search".to_string(),
1430        description: "Fetch web pages or search the web. If query starts with http:// or https://, fetches that URL directly and returns the page content as text. Otherwise, searches DuckDuckGo and returns top 5 results. Use this to read RFCs, protocol specifications, or documentation. Note: This makes external network requests.".to_string(),
1431        parameters: vec![Parameter {
1432            name: "query".to_string(),
1433            type_hint: "string".to_string(),
1434            description: "URL to fetch (e.g., 'https://datatracker.ietf.org/doc/html/rfc7168') or search query (e.g., 'RFC 959 FTP protocol specification')".to_string(),
1435            required: true,
1436        }],
1437        example: json!({
1438            "type": "web_search",
1439            "query": "https://datatracker.ietf.org/doc/html/rfc7168"
1440        }),
1441        log_template: None,
1442    }
1443}
1444
1445/// Get protocol documentation action definition (DEPRECATED - use read_server_documentation_action or read_client_documentation_action)
1446pub fn read_base_stack_docs_action() -> ActionDefinition {
1447    ActionDefinition {
1448        name: "read_base_stack_docs".to_string(),
1449        description: "Get detailed documentation for a specific network protocol. Returns comprehensive information including description, startup parameters, examples, and keywords. Use this before starting a server to understand protocol configuration options.".to_string(),
1450        parameters: vec![Parameter {
1451            name: "protocol".to_string(),
1452            type_hint: "string".to_string(),
1453            description: "Protocol name (e.g., 'http', 'ssh', 'tor', 'dns'). Use lowercase.".to_string(),
1454            required: true,
1455        }],
1456        example: json!({
1457            "type": "read_base_stack_docs",
1458            "protocol": "tor"
1459        }),
1460        log_template: None,
1461    }
1462}
1463
1464/// Get unified protocol documentation action definition
1465///
1466/// This unified tool replaces both `read_server_documentation` and `read_client_documentation`.
1467/// It looks up protocols in both server and client registries and explains when to use each mode.
1468pub fn read_documentation_action() -> ActionDefinition {
1469    // Get all available server protocols from registry
1470    let server_registry = crate::protocol::server_registry::registry();
1471    let mut server_protocols: Vec<String> = server_registry
1472        .all_protocols()
1473        .into_iter()
1474        .filter(|(_, protocol)| {
1475            server_registry
1476                .metadata(protocol.protocol_name())
1477                .map(|m| m.is_available_to_llm())
1478                .unwrap_or(true)
1479        })
1480        .map(|(name, _)| name)
1481        .collect();
1482    server_protocols.sort();
1483
1484    // Get all available client protocols from registry
1485    let client_registry = &crate::protocol::client_registry::CLIENT_REGISTRY;
1486    let mut client_protocols: Vec<String> = client_registry.list_protocols();
1487    client_protocols.sort();
1488
1489    let server_list = if server_protocols.is_empty() {
1490        "None".to_string()
1491    } else {
1492        server_protocols.join(", ")
1493    };
1494
1495    let client_list = if client_protocols.is_empty() {
1496        "None".to_string()
1497    } else {
1498        client_protocols.join(", ")
1499    };
1500
1501    // Build example protocols array with all available protocols
1502    let mut all_protocols: Vec<String> = server_protocols.clone();
1503    for client_proto in &client_protocols {
1504        if !all_protocols
1505            .iter()
1506            .any(|s| s.eq_ignore_ascii_case(client_proto))
1507        {
1508            all_protocols.push(client_proto.clone());
1509        }
1510    }
1511    all_protocols.sort();
1512
1513    ActionDefinition {
1514        name: "read_documentation".to_string(),
1515        description: format!(
1516            r#"Get detailed protocol documentation. After you fetch documentation, you will be able to open a server or a client.
1517
1518## Available Protocols
1519
1520**Server protocols**: {}
1521
1522**Client protocols**: {}"#,
1523            server_list, client_list
1524        ),
1525        parameters: vec![Parameter {
1526            name: "protocols".to_string(),
1527            type_hint: "array".to_string(),
1528            description: "Array of protocol names to get documentation for. Maximum 5 protocols per call. Returns both server and client docs if available for each protocol.".to_string(),
1529            required: true,
1530        }],
1531        example: json!({
1532            "type": "read_documentation",
1533            "protocols": all_protocols
1534        }),
1535        log_template: None,
1536    }
1537}
1538
1539/// Get list models action definition
1540pub fn list_models_action() -> ActionDefinition {
1541    ActionDefinition {
1542        name: "list_models".to_string(),
1543        description: "List all available Ollama models that can be used for LLM generation. Returns a list of model names that can be used with the change_model action. Use this to discover which models are available before switching models.".to_string(),
1544        parameters: vec![],
1545        example: json!({
1546            "type": "list_models"
1547        }),
1548        log_template: None,
1549    }
1550}
1551
1552/// Get generate_random action definition
1553pub fn generate_random_action() -> ActionDefinition {
1554    ActionDefinition {
1555        name: "generate_random".to_string(),
1556        description: "Generate random data of various types. IMPORTANT: LLMs cannot generate truly random data - you MUST use this tool whenever you need random/mock data for responses. Supports: UUIDs, numbers, strings, emails, IPs, dates, lorem ipsum text, and more. This tool returns the random value which you can then use in your response.".to_string(),
1557        parameters: vec![
1558            Parameter {
1559                name: "data_type".to_string(),
1560                type_hint: "string".to_string(),
1561                description: "Type of random data: uuid, integer, float, string, hex, base64, word, sentence, paragraph, email, ipv4, ipv6, mac, port, timestamp, date, boolean, choice, choices".to_string(),
1562                required: true,
1563            },
1564            Parameter {
1565                name: "length".to_string(),
1566                type_hint: "number".to_string(),
1567                description: "Optional: Length for strings (default: 16), number of words for sentences (default: 10), or sentences for paragraphs (default: 5)".to_string(),
1568                required: false,
1569            },
1570            Parameter {
1571                name: "min".to_string(),
1572                type_hint: "number".to_string(),
1573                description: "Optional: Minimum value for integer/float (default: 0 for int, 0.0 for float), or min timestamp".to_string(),
1574                required: false,
1575            },
1576            Parameter {
1577                name: "max".to_string(),
1578                type_hint: "number".to_string(),
1579                description: "Optional: Maximum value for integer/float (default: 100 for int, 1.0 for float), or max timestamp".to_string(),
1580                required: false,
1581            },
1582            Parameter {
1583                name: "charset".to_string(),
1584                type_hint: "string".to_string(),
1585                description: "Optional: Character set for strings - alphanumeric (default), hex, digits, letters, lowercase, uppercase".to_string(),
1586                required: false,
1587            },
1588            Parameter {
1589                name: "choices".to_string(),
1590                type_hint: "array".to_string(),
1591                description: "Optional: Array of values to choose from (required for choice/choices types)".to_string(),
1592                required: false,
1593            },
1594            Parameter {
1595                name: "count".to_string(),
1596                type_hint: "number".to_string(),
1597                description: "Optional: Number of items to pick for 'choices' type (default: 1)".to_string(),
1598                required: false,
1599            },
1600        ],
1601        example: json!({
1602            "type": "generate_random",
1603            "data_type": "uuid"
1604        }),
1605        log_template: None,
1606    }
1607}
1608
1609/// Get action definition for list_tasks
1610pub fn list_tasks_action() -> ActionDefinition {
1611    ActionDefinition {
1612        name: "list_tasks".to_string(),
1613        description: "List all currently scheduled tasks. Returns information about all one-shot and recurring tasks, including their status, next execution time, and configuration.".to_string(),
1614        parameters: vec![],
1615        example: json!({
1616            "type": "list_tasks"
1617        }),
1618        log_template: None,
1619    }
1620}
1621
1622/// Get action definition for execute_sql
1623#[cfg(feature = "sqlite")]
1624pub fn execute_sql_action() -> ActionDefinition {
1625    ActionDefinition {
1626        name: "execute_sql".to_string(),
1627        description: "Execute a SQL query on a database. Supports DDL (CREATE/ALTER/DROP), DML (INSERT/UPDATE/DELETE), and DQL (SELECT). Returns results as JSON with columns and rows for SELECT queries, or affected row count for modifications.".to_string(),
1628        parameters: vec![
1629            Parameter {
1630                name: "database_id".to_string(),
1631                type_hint: "number".to_string(),
1632                description: "Database ID (from create_database response or list_databases). Format: db-N → use N.".to_string(),
1633                required: true,
1634            },
1635            Parameter {
1636                name: "query".to_string(),
1637                type_hint: "string".to_string(),
1638                description: "SQL query to execute. Use standard SQLite syntax. Be careful with semicolons (only one statement per execute_sql).".to_string(),
1639                required: true,
1640            },
1641        ],
1642        example: json!({
1643            "type": "execute_sql",
1644            "database_id": 1,
1645            "query": "SELECT * FROM files WHERE path LIKE '/home/%'"
1646        }),
1647        log_template: None,
1648    }
1649}
1650
1651/// Get action definition for list_databases
1652#[cfg(feature = "sqlite")]
1653pub fn list_databases_action() -> ActionDefinition {
1654    ActionDefinition {
1655        name: "list_databases".to_string(),
1656        description: "List all active SQLite databases with their schemas, table information, and row counts. Use this to discover available databases and understand their structure before querying.".to_string(),
1657        parameters: vec![],
1658        example: json!({
1659            "type": "list_databases"
1660        }),
1661        log_template: None,
1662    }
1663}
1664
1665/// Get all tool action definitions
1666pub fn get_all_tool_actions(
1667    web_search_mode: crate::state::app_state::WebSearchMode,
1668) -> Vec<ActionDefinition> {
1669    use crate::state::app_state::WebSearchMode;
1670
1671    let mut actions = vec![
1672        generate_random_action(), // Put first - LLMs need this for mock data
1673        read_file_action(),
1674        read_documentation_action(), // Unified tool for both server and client docs
1675        list_tasks_action(),
1676    ];
1677
1678    // Add SQLite tools if feature is enabled
1679    #[cfg(feature = "sqlite")]
1680    {
1681        actions.push(execute_sql_action());
1682        actions.push(list_databases_action());
1683    }
1684
1685    // Include web search tool for both ON and ASK modes (not for OFF)
1686    match web_search_mode {
1687        WebSearchMode::On | WebSearchMode::Ask => {
1688            actions.push(web_search_action());
1689        }
1690        WebSearchMode::Off => {
1691            // Don't include web search tool
1692        }
1693    }
1694
1695    actions
1696}
1697
1698/// Tools for scheduled tasks scoped to an already-running server or client.
1699///
1700/// The legacy name is retained for callers. Immediate network-event responses
1701/// exclude these tools through `PromptBuilder::advertised_network_event_actions`;
1702/// scheduled tasks may still read files, query SQLite, or gather other information.
1703/// Documentation tools for opening new instances belong to operator conversations.
1704pub fn get_network_event_tool_actions(
1705    web_search_mode: crate::state::app_state::WebSearchMode,
1706) -> Vec<ActionDefinition> {
1707    use crate::state::app_state::WebSearchMode;
1708
1709    let mut actions = vec![
1710        generate_random_action(),
1711        read_file_action(),
1712        list_tasks_action(),
1713    ];
1714
1715    // Add SQLite tools if feature is enabled
1716    #[cfg(feature = "sqlite")]
1717    {
1718        actions.push(execute_sql_action());
1719        actions.push(list_databases_action());
1720    }
1721
1722    // Include web search tool for both ON and ASK modes (not for OFF)
1723    match web_search_mode {
1724        WebSearchMode::On | WebSearchMode::Ask => {
1725            actions.push(web_search_action());
1726        }
1727        WebSearchMode::Off => {
1728            // Don't include web search tool
1729        }
1730    }
1731
1732    actions
1733}
1734
1735/// Execute a tool action
1736pub async fn execute_tool(
1737    action: &ToolAction,
1738    approval_tx: Option<
1739        &tokio::sync::mpsc::UnboundedSender<crate::state::app_state::WebApprovalRequest>,
1740    >,
1741    web_search_mode: crate::state::app_state::WebSearchMode,
1742    _state: Option<&crate::state::AppState>,
1743) -> ToolResult {
1744    use crate::state::app_state::{WebApprovalRequest, WebApprovalResponse, WebSearchMode};
1745
1746    match action {
1747        ToolAction::ReadFile {
1748            path,
1749            mode,
1750            lines,
1751            pattern,
1752            context_before,
1753            context_after,
1754        } => {
1755            execute_read_file(
1756                path,
1757                mode,
1758                *lines,
1759                pattern.as_deref(),
1760                *context_before,
1761                *context_after,
1762            )
1763            .await
1764        }
1765        ToolAction::WebSearch { query } => {
1766            // Check if approval is needed (ASK mode)
1767            if web_search_mode == WebSearchMode::Ask {
1768                if let Some(tx) = approval_tx {
1769                    debug!("Web search in ASK mode, requesting approval for: {}", query);
1770
1771                    // Create oneshot channel for response
1772                    let (response_tx, response_rx) = tokio::sync::oneshot::channel();
1773
1774                    // Send approval request to UI
1775                    let request = WebApprovalRequest {
1776                        url: query.to_string(),
1777                        response_tx,
1778                    };
1779
1780                    if let Err(e) = tx.send(request) {
1781                        error!("Failed to send web approval request: {}", e);
1782                        return ToolResult::error(
1783                            "web_search",
1784                            query.to_string(),
1785                            "Failed to request approval for web search".to_string(),
1786                        );
1787                    }
1788
1789                    // Wait for user response
1790                    match response_rx.await {
1791                        Ok(WebApprovalResponse::Allow) => {
1792                            debug!("User approved web search");
1793                            // Proceed with search
1794                            execute_web_search(query).await
1795                        }
1796                        Ok(WebApprovalResponse::AlwaysAllow) => {
1797                            debug!("User chose always allow (note: mode switch happens in UI)");
1798                            // UI will switch mode to ON, proceed with this search
1799                            execute_web_search(query).await
1800                        }
1801                        Ok(WebApprovalResponse::Deny) => {
1802                            debug!("User denied web search");
1803                            ToolResult::error(
1804                                "web_search",
1805                                query.to_string(),
1806                                "Web search denied by user".to_string(),
1807                            )
1808                        }
1809                        Err(e) => {
1810                            error!("Failed to receive approval response: {}", e);
1811                            ToolResult::error(
1812                                "web_search",
1813                                query.to_string(),
1814                                "Approval request was cancelled".to_string(),
1815                            )
1816                        }
1817                    }
1818                } else {
1819                    error!("Web search in ASK mode but no approval channel available");
1820                    ToolResult::error(
1821                        "web_search",
1822                        query.to_string(),
1823                        "Cannot request approval: approval channel not configured".to_string(),
1824                    )
1825                }
1826            } else {
1827                // ON mode - proceed directly
1828                execute_web_search(query).await
1829            }
1830        }
1831        ToolAction::ReadBaseStackDocs { protocol } => execute_read_base_stack_docs(protocol).await,
1832        ToolAction::ReadServerDocumentation {
1833            protocols,
1834            protocol,
1835        } => {
1836            let all_protocols = ToolAction::merge_protocols(protocols, protocol);
1837            execute_read_server_documentation(&all_protocols).await
1838        }
1839        ToolAction::ReadClientDocumentation {
1840            protocols,
1841            protocol,
1842        } => {
1843            let all_protocols = ToolAction::merge_protocols(protocols, protocol);
1844            execute_read_client_documentation(&all_protocols).await
1845        }
1846        ToolAction::ReadDocumentation {
1847            protocols,
1848            protocol,
1849        } => {
1850            let all_protocols = ToolAction::merge_protocols(protocols, protocol);
1851            execute_read_documentation(&all_protocols).await
1852        }
1853        ToolAction::ListModels => execute_list_models().await,
1854        ToolAction::GenerateRandom {
1855            data_type,
1856            length,
1857            min,
1858            max,
1859            charset,
1860            choices,
1861            count,
1862        } => {
1863            execute_generate_random(
1864                data_type,
1865                *length,
1866                *min,
1867                *max,
1868                charset.as_deref(),
1869                choices.as_deref(),
1870                *count,
1871            )
1872            .await
1873        }
1874        ToolAction::ListTasks => {
1875            if let Some(state) = _state {
1876                execute_list_tasks(state).await
1877            } else {
1878                ToolResult::error(
1879                    "list_tasks",
1880                    "".to_string(),
1881                    "State not available".to_string(),
1882                )
1883            }
1884        }
1885        #[cfg(feature = "sqlite")]
1886        ToolAction::ExecuteSql { database_id, query } => {
1887            if let Some(state) = _state {
1888                execute_execute_sql(state, *database_id, query).await
1889            } else {
1890                ToolResult::error(
1891                    "execute_sql",
1892                    query.to_string(),
1893                    "State not available".to_string(),
1894                )
1895            }
1896        }
1897        #[cfg(feature = "sqlite")]
1898        ToolAction::ListDatabases => {
1899            if let Some(state) = _state {
1900                execute_list_databases(state).await
1901            } else {
1902                ToolResult::error(
1903                    "list_databases",
1904                    "".to_string(),
1905                    "State not available".to_string(),
1906                )
1907            }
1908        }
1909    }
1910}
1911
1912/// Execute read_base_stack_docs tool
1913async fn execute_read_base_stack_docs(protocol: &str) -> ToolResult {
1914    use tracing::info;
1915
1916    info!("🔧 Tool: read_base_stack_docs - protocol=\"{}\"", protocol);
1917    debug!("Getting documentation for protocol: {}", protocol);
1918
1919    // Get structured documentation data
1920    let doc_data = match super::common::generate_single_protocol_doc_data(protocol) {
1921        Ok(data) => data,
1922        Err(e) => {
1923            warn!(
1924                "Failed to get documentation for protocol '{}': {}",
1925                protocol, e
1926            );
1927            info!("  ✗ Protocol '{}' not found: {}", protocol, e);
1928            return ToolResult::error(
1929                "read_base_stack_docs",
1930                protocol.to_string(),
1931                format!("Protocol not found or unavailable: {}", e),
1932            );
1933        }
1934    };
1935
1936    // Render documentation using the template
1937    let template_engine = &crate::llm::template_engine::TEMPLATE_ENGINE;
1938    let rendered = match template_engine.render(
1939        "shared/partials/base_stack_docs",
1940        &serde_json::json!({ "base_stack_docs": doc_data }),
1941    ) {
1942        Ok(docs) => docs,
1943        Err(e) => {
1944            error!("Failed to render base stack docs template: {}", e);
1945            return ToolResult::error(
1946                "read_base_stack_docs",
1947                protocol.to_string(),
1948                format!("Failed to render documentation: {}", e),
1949            );
1950        }
1951    };
1952
1953    debug!(
1954        "Successfully retrieved documentation for protocol '{}' ({} bytes)",
1955        protocol,
1956        rendered.len()
1957    );
1958    info!(
1959        "  ✓ Retrieved docs for '{}' ({} bytes)",
1960        protocol,
1961        rendered.len()
1962    );
1963
1964    // Append open_server action description to inform LLM it's now enabled
1965    let mut result = rendered;
1966    result.push_str("\n\n---\n\n");
1967    result.push_str("## open_server Action\n\n");
1968    result.push_str("**Action:** `open_server`\n\n");
1969    result
1970        .push_str("**Description:** Start a new server with the protocol you just read about.\n\n");
1971    result.push_str("**Required Parameters:**\n");
1972    result.push_str("- `port` (number): Port number to listen on\n");
1973    result.push_str(
1974        "- `base_stack` (string): Protocol stack to use (e.g., the protocol you just read about)\n",
1975    );
1976    result.push_str(
1977        "- `instruction` (string): Detailed instructions for handling network events\n\n",
1978    );
1979    result.push_str("**Optional Parameters:**\n");
1980    result.push_str("- `send_first` (boolean): True if server sends data first (FTP, SMTP), false if it waits for client (HTTP)\n");
1981    result.push_str("- `initial_memory` (string): Initial memory as a string for persistent context across connections\n");
1982    result.push_str("- `startup_params` (object): Protocol-specific startup parameters (see protocol documentation above)\n");
1983    result.push_str("- `scheduled_tasks` (array): Scheduled tasks to create with this server\n");
1984    result.push_str("- Script-related parameters (if scripting is enabled)\n\n");
1985    result.push_str("**Example:**\n");
1986    result.push_str("```json\n");
1987    result.push_str("{\n");
1988    result.push_str("  \"type\": \"open_server\",\n");
1989    result.push_str("  \"port\": 8080,\n");
1990    result.push_str(&format!(
1991        "  \"base_stack\": \"{}\",\n",
1992        protocol.to_lowercase()
1993    ));
1994    result.push_str("  \"instruction\": \"Handle requests according to protocol specification\"\n");
1995    result.push_str("}\n");
1996    result.push_str("```\n");
1997
1998    ToolResult::success("read_base_stack_docs", protocol.to_string(), result)
1999}
2000
2001/// Execute read_server_documentation tool
2002async fn execute_read_server_documentation(protocols: &[String]) -> ToolResult {
2003    use tracing::info;
2004
2005    if protocols.is_empty() {
2006        return ToolResult::error(
2007            "read_server_documentation",
2008            "no protocols".to_string(),
2009            "No protocols specified. Provide at least one protocol name in the 'protocols' array."
2010                .to_string(),
2011        );
2012    }
2013
2014    info!(
2015        "🔧 Tool: read_server_documentation - protocols={:?}",
2016        protocols
2017    );
2018    debug!(
2019        "Getting server documentation for protocols: {:?}",
2020        protocols
2021    );
2022
2023    let registry = crate::protocol::server_registry::registry();
2024    let mut result = String::new();
2025    let mut found_protocols = Vec::new();
2026    let mut not_found_protocols = Vec::new();
2027
2028    for protocol in protocols {
2029        // Get server protocol from registry
2030        let server_protocol = match registry.get(protocol) {
2031            Some(p) => p,
2032            None => {
2033                warn!("Server protocol '{}' not found", protocol);
2034                not_found_protocols.push(protocol.clone());
2035                continue;
2036            }
2037        };
2038
2039        found_protocols.push(protocol.clone());
2040
2041        // Build documentation from protocol
2042        let metadata = server_protocol.metadata();
2043        let startup_params = server_protocol.get_startup_parameters();
2044
2045        result.push_str(&format!("# Server Protocol: {}\n\n", protocol));
2046        result.push_str(&format!("**Stack:** {}\n", server_protocol.stack_name()));
2047        result.push_str(&format!("**Group:** {}\n", server_protocol.group_name()));
2048        result.push_str(&format!("**State:** {:?}\n\n", metadata.state));
2049
2050        result.push_str("## Description\n\n");
2051        result.push_str(&format!("{}\n\n", server_protocol.description()));
2052
2053        if !startup_params.is_empty() {
2054            result.push_str("## Startup Parameters\n\n");
2055            for param in &startup_params {
2056                result.push_str(&format!(
2057                    "- **{}** ({}): {}\n",
2058                    param.name, param.type_hint, param.description
2059                ));
2060                if param.required {
2061                    result.push_str("  - Required: Yes\n");
2062                }
2063            }
2064            result.push_str("\n");
2065        }
2066
2067        result.push_str("## Example Prompt\n\n");
2068        result.push_str(&format!("{}\n\n", server_protocol.example_prompt()));
2069
2070        if !server_protocol.keywords().is_empty() {
2071            result.push_str("## Keywords\n\n");
2072            result.push_str(&format!("{}\n\n", server_protocol.keywords().join(", ")));
2073        }
2074
2075        if let Some(notes) = metadata.notes {
2076            result.push_str("## Notes\n\n");
2077            result.push_str(&format!("{}\n\n", notes));
2078        }
2079
2080        // Concrete, ready-to-run `open_server` examples for THIS protocol, in each
2081        // handler mode. These come from the protocol's own `get_startup_examples()`
2082        // (the existing `StartupExamples` machinery) so they use real base_stack names,
2083        // real event patterns and — for script/static mode — real protocol action types.
2084        // Shown only for the protocol the model actually asked about, so they cost
2085        // nothing on the always-on system prompt.
2086        result.push_str(&server_protocol.get_startup_examples().to_prompt_text());
2087
2088        // Add protocol-specific event examples if available
2089        let event_types = server_protocol.get_event_types();
2090        if !event_types.is_empty() {
2091            result.push_str("## Protocol-Specific Response Examples\n\n");
2092            result.push_str("**IMPORTANT**: Use these protocol-specific action types when responding to events.\n\n");
2093
2094            for event_type in event_types {
2095                result.push_str(&format!("### Event: {}\n\n", event_type.id));
2096
2097                // Response example is always present (required field)
2098                result.push_str("**Response Example:**\n```json\n");
2099                result.push_str(
2100                    &serde_json::to_string_pretty(&event_type.effective_response_example())
2101                        .unwrap_or_default(),
2102                );
2103                result.push_str("\n```\n\n");
2104
2105                if !event_type.alternative_examples.is_empty() {
2106                    result.push_str("**Alternative Responses:**\n");
2107                    for example in &event_type.alternative_examples {
2108                        result.push_str("```json\n");
2109                        result.push_str(&serde_json::to_string_pretty(example).unwrap_or_default());
2110                        result.push_str("\n```\n");
2111                    }
2112                    result.push_str("\n");
2113                }
2114            }
2115        }
2116
2117        result.push_str("\n---\n\n");
2118    }
2119
2120    if found_protocols.is_empty() {
2121        return ToolResult::error(
2122            "read_server_documentation",
2123            protocols.join(", "),
2124            format!(
2125                "No valid server protocols found. Protocols not found: {:?}. Use read_server_documentation to see available server protocols.",
2126                not_found_protocols
2127            ),
2128        );
2129    }
2130
2131    // Add open_server action description once at the end. The concrete, ready-to-run
2132    // `open_server` examples are emitted per protocol above (via each protocol's own
2133    // `get_startup_examples()`), so there is no need for a generic stub here.
2134    result.push_str("## open_server Action (Now Enabled)\n\n");
2135    result.push_str(
2136        "The `open_server` action is now enabled for the protocols documented above. \
2137         Use the LLM-mode / Script-mode / Static-mode examples shown under each protocol \
2138         as your starting point.\n\n",
2139    );
2140
2141    if !not_found_protocols.is_empty() {
2142        result.push_str(&format!(
2143            "\n**Note:** The following protocols were not found: {:?}\n",
2144            not_found_protocols
2145        ));
2146    }
2147
2148    debug!(
2149        "Successfully retrieved server documentation for {} protocols ({} bytes)",
2150        found_protocols.len(),
2151        result.len()
2152    );
2153    info!(
2154        "  ✓ Retrieved server docs for {:?} ({} bytes)",
2155        found_protocols,
2156        result.len()
2157    );
2158
2159    ToolResult::success(
2160        "read_server_documentation",
2161        found_protocols.join(", "),
2162        result,
2163    )
2164}
2165
2166/// Execute read_client_documentation tool
2167async fn execute_read_client_documentation(protocols: &[String]) -> ToolResult {
2168    use tracing::info;
2169
2170    if protocols.is_empty() {
2171        return ToolResult::error(
2172            "read_client_documentation",
2173            "no protocols".to_string(),
2174            "No protocols specified. Provide at least one protocol name in the 'protocols' array."
2175                .to_string(),
2176        );
2177    }
2178
2179    info!(
2180        "🔧 Tool: read_client_documentation - protocols={:?}",
2181        protocols
2182    );
2183    debug!(
2184        "Getting client documentation for protocols: {:?}",
2185        protocols
2186    );
2187
2188    let client_registry = &crate::protocol::client_registry::CLIENT_REGISTRY;
2189    let mut result = String::new();
2190    let mut found_protocols = Vec::new();
2191    let mut not_found_protocols = Vec::new();
2192
2193    for protocol in protocols {
2194        // Get client protocol from registry
2195        let client_protocol = match client_registry.get(protocol) {
2196            Some(p) => p,
2197            None => {
2198                warn!("Client protocol '{}' not found", protocol);
2199                not_found_protocols.push(protocol.clone());
2200                continue;
2201            }
2202        };
2203
2204        found_protocols.push(protocol.clone());
2205
2206        // Build documentation from protocol
2207        let metadata = client_protocol.metadata();
2208        let startup_params = client_protocol.get_startup_parameters();
2209
2210        result.push_str(&format!("# Client Protocol: {}\n\n", protocol));
2211        result.push_str(&format!("**Stack:** {}\n", client_protocol.stack_name()));
2212        result.push_str(&format!("**Group:** {}\n", client_protocol.group_name()));
2213        result.push_str(&format!("**State:** {:?}\n\n", metadata.state));
2214
2215        result.push_str("## Description\n\n");
2216        result.push_str(&format!("{}\n\n", client_protocol.description()));
2217
2218        if !startup_params.is_empty() {
2219            result.push_str("## Startup Parameters\n\n");
2220            for param in &startup_params {
2221                result.push_str(&format!(
2222                    "- **{}** ({}): {}\n",
2223                    param.name, param.type_hint, param.description
2224                ));
2225                if param.required {
2226                    result.push_str("  - Required: Yes\n");
2227                }
2228            }
2229            result.push_str("\n");
2230        }
2231
2232        result.push_str("## Example Prompt\n\n");
2233        result.push_str(&format!("{}\n\n", client_protocol.example_prompt()));
2234
2235        if !client_protocol.keywords().is_empty() {
2236            result.push_str("## Keywords\n\n");
2237            result.push_str(&format!("{}\n\n", client_protocol.keywords().join(", ")));
2238        }
2239
2240        if let Some(notes) = metadata.notes {
2241            result.push_str("## Notes\n\n");
2242            result.push_str(&format!("{}\n\n", notes));
2243        }
2244
2245        // Add protocol-specific event examples if available
2246        let event_types = client_protocol.get_event_types();
2247        if !event_types.is_empty() {
2248            result.push_str("## Protocol-Specific Action Examples\n\n");
2249            result.push_str("**IMPORTANT**: Use these protocol-specific action types when sending requests.\n\n");
2250
2251            for event_type in event_types {
2252                result.push_str(&format!("### Event: {}\n\n", event_type.id));
2253
2254                // Response example is always present (required field)
2255                result.push_str("**Action Example:**\n```json\n");
2256                result.push_str(
2257                    &serde_json::to_string_pretty(&event_type.effective_response_example())
2258                        .unwrap_or_default(),
2259                );
2260                result.push_str("\n```\n\n");
2261
2262                if !event_type.alternative_examples.is_empty() {
2263                    result.push_str("**Alternative Actions:**\n");
2264                    for example in &event_type.alternative_examples {
2265                        result.push_str("```json\n");
2266                        result.push_str(&serde_json::to_string_pretty(example).unwrap_or_default());
2267                        result.push_str("\n```\n");
2268                    }
2269                    result.push_str("\n");
2270                }
2271            }
2272        }
2273
2274        result.push_str("\n---\n\n");
2275    }
2276
2277    if found_protocols.is_empty() {
2278        return ToolResult::error(
2279            "read_client_documentation",
2280            protocols.join(", "),
2281            format!(
2282                "No valid client protocols found. Protocols not found: {:?}. Use read_client_documentation to see available client protocols.",
2283                not_found_protocols
2284            ),
2285        );
2286    }
2287
2288    // Add open_client action description once at the end
2289    result.push_str("## open_client Action (Now Enabled)\n\n");
2290    result.push_str(
2291        "The `open_client` action is now enabled for the protocols documented above.\n\n",
2292    );
2293
2294    // Add examples for each documented protocol
2295    for protocol in &found_protocols {
2296        result.push_str(&format!("**Example for {}:**\n```json\n{{\n", protocol));
2297        result.push_str("  \"type\": \"open_client\",\n");
2298        result.push_str(&format!("  \"protocol\": \"{}\",\n", protocol));
2299        result.push_str("  \"remote_addr\": \"example.com:80\",\n");
2300        result.push_str("  \"instruction\": \"Connect and interact with the remote server\"\n");
2301        result.push_str("}\n```\n\n");
2302    }
2303
2304    if !not_found_protocols.is_empty() {
2305        result.push_str(&format!(
2306            "\n**Note:** The following protocols were not found: {:?}\n",
2307            not_found_protocols
2308        ));
2309    }
2310
2311    debug!(
2312        "Successfully retrieved client documentation for {} protocols ({} bytes)",
2313        found_protocols.len(),
2314        result.len()
2315    );
2316    info!(
2317        "  ✓ Retrieved client docs for {:?} ({} bytes)",
2318        found_protocols,
2319        result.len()
2320    );
2321
2322    ToolResult::success(
2323        "read_client_documentation",
2324        found_protocols.join(", "),
2325        result,
2326    )
2327}
2328
2329/// Execute unified read_documentation tool
2330///
2331/// This function looks up protocols in both server and client registries,
2332/// returning comprehensive documentation for all found modes.
2333async fn execute_read_documentation(protocols: &[String]) -> ToolResult {
2334    use tracing::info;
2335
2336    if protocols.is_empty() {
2337        return ToolResult::error(
2338            "read_documentation",
2339            "no protocols".to_string(),
2340            "No protocols specified. Provide at least one protocol name in the 'protocols' array."
2341                .to_string(),
2342        );
2343    }
2344
2345    if protocols.len() > 5 {
2346        return ToolResult::error(
2347            "read_documentation",
2348            format!("{} protocols requested", protocols.len()),
2349            format!(
2350                "Too many protocols requested ({}). Maximum is 5 per call. Please retry with 5 or fewer protocols. Requested: {:?}",
2351                protocols.len(),
2352                protocols
2353            ),
2354        );
2355    }
2356
2357    info!("🔧 Tool: read_documentation - protocols={:?}", protocols);
2358    debug!("Getting documentation for protocols: {:?}", protocols);
2359
2360    let server_registry = crate::protocol::server_registry::registry();
2361    let client_registry = &crate::protocol::client_registry::CLIENT_REGISTRY;
2362
2363    let mut result = String::new();
2364    let mut found_server_protocols = Vec::new();
2365    let mut found_client_protocols = Vec::new();
2366    let mut not_found_protocols = Vec::new();
2367
2368    // Add guidance header with explicit keyword matching
2369    result.push_str("# Protocol Documentation\n\n");
2370    result.push_str("## CRITICAL: When to Use Server vs Client Mode\n\n");
2371    result.push_str(
2372        "**Server Mode (open_server)** - Use when the user wants to HOST/SERVE content:\n",
2373    );
2374    result.push_str("- Keywords: \"serve\", \"host\", \"listen\", \"start a server\", \"create a server\", \"run a server\", \"open server\", \"provide\", \"respond to\"\n");
2375    result.push_str("- You LISTEN on a port and RESPOND to incoming requests\n");
2376    result.push_str("- User wants to PROVIDE a service that others connect to\n");
2377    result.push_str("- Examples: \"host a website\", \"start HTTP server\", \"create DNS server\", \"run FTP server\"\n\n");
2378    result.push_str("**Client Mode (open_client)** - Use when the user wants to CONNECT to an existing remote server:\n");
2379    result.push_str("- Keywords: \"connect to\", \"fetch from\", \"query\", \"access\", \"call\", \"request from\", \"get from\", \"send to\", \"send a\"\n");
2380    result.push_str("- You CONNECT to a remote server and SEND requests to it\n");
2381    result.push_str("- User wants to ACCESS a service that someone else is running\n");
2382    result.push_str("- Examples: \"connect to Redis at localhost:6379\", \"fetch from API\", \"query database\"\n\n");
2383    result.push_str("**⚠️ IMPORTANT**: If the user says \"serve\", \"host\", or \"provide\" content, use `open_server` even if they mistakenly say \"client\".\n");
2384    result.push_str("The ACTION (serving content) matters more than the word choice.\n\n");
2385    result.push_str("---\n\n");
2386
2387    for protocol in protocols {
2388        // Normalize protocol name (try both cases)
2389        let protocol_upper = protocol.to_uppercase();
2390        let protocol_lower = protocol.to_lowercase();
2391
2392        // Exact registry key first (e.g. "HTTP", "S3"), then fall back to keyword
2393        // resolution so mixed-case keys ("DynamoDB") and keyword/base_stack forms
2394        // ("dynamo", "dynamodb", "s3 bucket") still reach the docs — uppercasing
2395        // alone never matches "DynamoDB".
2396        let server_protocol = server_registry.get(&protocol_upper).or_else(|| {
2397            server_registry
2398                .parse_from_str(protocol)
2399                .and_then(|name| server_registry.get(&name))
2400        });
2401        let client_protocol = client_registry.get(&protocol_lower);
2402
2403        let found_any = server_protocol.is_some() || client_protocol.is_some();
2404
2405        if !found_any {
2406            warn!(
2407                "Protocol '{}' not found in server or client registry",
2408                protocol
2409            );
2410            not_found_protocols.push(protocol.clone());
2411            continue;
2412        }
2413
2414        // Document server mode if available
2415        if let Some(server) = server_protocol {
2416            // Canonical registry name (e.g. "DynamoDB"), not the uppercased input.
2417            let server_name = server.protocol_name();
2418            found_server_protocols.push(server_name.to_string());
2419
2420            let metadata = server.metadata();
2421            let startup_params = server.get_startup_parameters();
2422
2423            result.push_str(&format!("# {} (Server Mode)\n\n", server_name));
2424            result.push_str("**Mode:** Server - listens for incoming connections\n");
2425            result.push_str(&format!("**Stack:** {}\n", server.stack_name()));
2426            result.push_str(&format!("**Group:** {}\n", server.group_name()));
2427            result.push_str(&format!("**State:** {:?}\n\n", metadata.state));
2428
2429            result.push_str("## Description\n\n");
2430            result.push_str(&format!("{}\n\n", server.description()));
2431
2432            if !startup_params.is_empty() {
2433                result.push_str("## Startup Parameters\n\n");
2434                for param in &startup_params {
2435                    result.push_str(&format!(
2436                        "- **{}** ({}): {}\n",
2437                        param.name, param.type_hint, param.description
2438                    ));
2439                    if param.required {
2440                        result.push_str("  - Required: Yes\n");
2441                    }
2442                }
2443                result.push_str("\n");
2444            }
2445
2446            result.push_str("## Example Prompt\n\n");
2447            result.push_str(&format!("{}\n\n", server.example_prompt()));
2448
2449            if !server.keywords().is_empty() {
2450                result.push_str("## Keywords\n\n");
2451                result.push_str(&format!("{}\n\n", server.keywords().join(", ")));
2452            }
2453
2454            if let Some(notes) = metadata.notes {
2455                result.push_str("## Notes\n\n");
2456                result.push_str(&format!("{}\n\n", notes));
2457            }
2458
2459            // Concrete `open_server` examples for THIS protocol in each handler
2460            // mode, from its own `get_startup_examples()`. Shown only for the
2461            // protocol the model asked about, so it costs nothing on the
2462            // always-on system prompt.
2463            result.push_str(&server.get_startup_examples().to_prompt_text());
2464
2465            // Add protocol-specific event examples
2466            let event_types = server.get_event_types();
2467            if !event_types.is_empty() {
2468                result.push_str("## Protocol-Specific Response Examples\n\n");
2469                for event_type in event_types {
2470                    result.push_str(&format!("### Event: {}\n\n", event_type.id));
2471                    result.push_str("**Response Example:**\n```json\n");
2472                    result.push_str(
2473                        &serde_json::to_string_pretty(&event_type.effective_response_example())
2474                            .unwrap_or_default(),
2475                    );
2476                    result.push_str("\n```\n\n");
2477                }
2478            }
2479
2480            result.push_str("\n---\n\n");
2481        }
2482
2483        // Document client mode if available
2484        if let Some(client) = client_protocol {
2485            found_client_protocols.push(protocol_lower.clone());
2486
2487            let metadata = client.metadata();
2488            let startup_params = client.get_startup_parameters();
2489
2490            result.push_str(&format!("# {} (Client Mode)\n\n", protocol_lower));
2491            result.push_str("**Mode:** Client - connects to remote servers\n");
2492            result.push_str(&format!("**Stack:** {}\n", client.stack_name()));
2493            result.push_str(&format!("**Group:** {}\n", client.group_name()));
2494            result.push_str(&format!("**State:** {:?}\n\n", metadata.state));
2495
2496            result.push_str("## Description\n\n");
2497            result.push_str(&format!("{}\n\n", client.description()));
2498
2499            if !startup_params.is_empty() {
2500                result.push_str("## Startup Parameters\n\n");
2501                for param in &startup_params {
2502                    result.push_str(&format!(
2503                        "- **{}** ({}): {}\n",
2504                        param.name, param.type_hint, param.description
2505                    ));
2506                    if param.required {
2507                        result.push_str("  - Required: Yes\n");
2508                    }
2509                }
2510                result.push_str("\n");
2511            }
2512
2513            result.push_str("## Example Prompt\n\n");
2514            result.push_str(&format!("{}\n\n", client.example_prompt()));
2515
2516            if !client.keywords().is_empty() {
2517                result.push_str("## Keywords\n\n");
2518                result.push_str(&format!("{}\n\n", client.keywords().join(", ")));
2519            }
2520
2521            if let Some(notes) = metadata.notes {
2522                result.push_str("## Notes\n\n");
2523                result.push_str(&format!("{}\n\n", notes));
2524            }
2525
2526            // Add protocol-specific event examples
2527            let event_types = client.get_event_types();
2528            if !event_types.is_empty() {
2529                result.push_str("## Protocol-Specific Action Examples\n\n");
2530                for event_type in event_types {
2531                    result.push_str(&format!("### Event: {}\n\n", event_type.id));
2532                    result.push_str("**Action Example:**\n```json\n");
2533                    result.push_str(
2534                        &serde_json::to_string_pretty(&event_type.effective_response_example())
2535                            .unwrap_or_default(),
2536                    );
2537                    result.push_str("\n```\n\n");
2538                }
2539            }
2540
2541            result.push_str("\n---\n\n");
2542        }
2543    }
2544
2545    if found_server_protocols.is_empty() && found_client_protocols.is_empty() {
2546        return ToolResult::error(
2547            "read_documentation",
2548            protocols.join(", "),
2549            format!(
2550                "No valid protocols found. Protocols not found: {:?}",
2551                not_found_protocols
2552            ),
2553        );
2554    }
2555
2556    // Add enabled actions summary at the end
2557    result.push_str("## Enabled Actions\n\n");
2558
2559    // Add decision reminder before showing actions
2560    result
2561        .push_str("**⚠️ BEFORE CHOOSING**: Re-read the user's request. What do they want to DO?\n");
2562    result.push_str("- If they want to SERVE/HOST/PROVIDE content → use `open_server`\n");
2563    result
2564        .push_str("- If they want to CONNECT TO/FETCH FROM a remote server → use `open_client`\n");
2565    result.push_str("- The ACTION matters, not the words. \"serve recipes\" = server, even if user says \"client\"\n\n");
2566
2567    if !found_server_protocols.is_empty() {
2568        result.push_str("### open_server (For SERVING content)\n\n");
2569        result.push_str("Use when: User wants to HOST content, SERVE data, LISTEN for connections, or PROVIDE a service.\n\n");
2570        for protocol in &found_server_protocols {
2571            result.push_str(&format!("**Example for {} server:**\n```json\n", protocol));
2572            result.push_str("{\n");
2573            result.push_str("  \"type\": \"open_server\",\n");
2574            result.push_str("  \"port\": 8080,\n");
2575            result.push_str(&format!(
2576                "  \"base_stack\": \"{}\",\n",
2577                protocol.to_lowercase()
2578            ));
2579            result.push_str("  \"instruction\": \"Handle incoming requests\"\n");
2580            result.push_str("}\n```\n\n");
2581        }
2582    }
2583
2584    if !found_client_protocols.is_empty() {
2585        result.push_str("### open_client (For CONNECTING to remote servers)\n\n");
2586        result.push_str("Use when: User wants to CONNECT TO an existing server, FETCH data, QUERY a database, or ACCESS a remote service.\n\n");
2587        for protocol in &found_client_protocols {
2588            result.push_str(&format!("**Example for {} client:**\n```json\n", protocol));
2589            result.push_str("{\n");
2590            result.push_str("  \"type\": \"open_client\",\n");
2591            result.push_str(&format!("  \"protocol\": \"{}\",\n", protocol));
2592            result.push_str("  \"remote_addr\": \"example.com:80\",\n");
2593            result.push_str("  \"instruction\": \"Connect and interact\"\n");
2594            result.push_str("}\n```\n\n");
2595        }
2596    }
2597
2598    if !not_found_protocols.is_empty() {
2599        result.push_str(&format!(
2600            "\n**Note:** The following protocols were not found: {:?}\n",
2601            not_found_protocols
2602        ));
2603    }
2604
2605    let all_found: Vec<String> = found_server_protocols
2606        .iter()
2607        .chain(found_client_protocols.iter())
2608        .cloned()
2609        .collect();
2610
2611    debug!(
2612        "Successfully retrieved documentation for {} protocols ({} bytes)",
2613        all_found.len(),
2614        result.len()
2615    );
2616    info!(
2617        "  ✓ Retrieved docs for {:?} ({} bytes)",
2618        all_found,
2619        result.len()
2620    );
2621
2622    ToolResult::success("read_documentation", all_found.join(", "), result)
2623}
2624
2625/// Execute list_tasks tool
2626async fn execute_list_tasks(state: &crate::state::AppState) -> ToolResult {
2627    use tracing::info;
2628
2629    info!("🔧 Tool: list_tasks - listing all scheduled tasks");
2630
2631    let tasks = state.get_all_tasks().await;
2632    if tasks.is_empty() {
2633        ToolResult::success(
2634            "list_tasks",
2635            "no tasks".to_string(),
2636            "No scheduled tasks".to_string(),
2637        )
2638    } else {
2639        let task_count = tasks.len();
2640        let mut result = format!("{} scheduled task(s):\n", task_count);
2641        for task in &tasks {
2642            result.push_str(&format!("  {}\n", task.format_for_prompt()));
2643        }
2644
2645        info!("  ✓ Listed {} task(s)", task_count);
2646        ToolResult::success("list_tasks", task_count.to_string(), result)
2647    }
2648}
2649
2650/// Execute execute_sql tool
2651#[cfg(feature = "sqlite")]
2652async fn execute_execute_sql(
2653    state: &crate::state::AppState,
2654    database_id: u32,
2655    query: &str,
2656) -> ToolResult {
2657    use tracing::info;
2658
2659    info!(
2660        "🔧 Tool: execute_sql - db-{} query=\"{}\"",
2661        database_id, query
2662    );
2663
2664    let db_id = crate::state::DatabaseId::new(database_id);
2665
2666    match state.execute_sql(db_id, query).await {
2667        Ok(result) => {
2668            let formatted = result.format();
2669
2670            // Get updated database info
2671            let db_info = if let Some(db) = state.get_database(db_id).await {
2672                format!("\nDatabase: {}", db.schema_summary())
2673            } else {
2674                String::new()
2675            };
2676
2677            let output = format!("Query result:\n{}{}", formatted, db_info);
2678
2679            info!("  ✓ Query executed successfully");
2680            ToolResult::success("execute_sql", query.to_string(), output)
2681        }
2682        Err(e) => {
2683            let error_msg = format!("SQL error: {}", e);
2684            info!("  ✗ Query failed: {}", e);
2685            ToolResult::error("execute_sql", query.to_string(), error_msg)
2686        }
2687    }
2688}
2689
2690/// Execute list_databases tool
2691#[cfg(feature = "sqlite")]
2692async fn execute_list_databases(state: &crate::state::AppState) -> ToolResult {
2693    use tracing::info;
2694
2695    info!("🔧 Tool: list_databases - listing all databases");
2696
2697    let databases = state.get_all_databases().await;
2698    if databases.is_empty() {
2699        ToolResult::success(
2700            "list_databases",
2701            "no databases".to_string(),
2702            "No databases".to_string(),
2703        )
2704    } else {
2705        let db_count = databases.len();
2706        let mut result = format!("{} database(s):\n", db_count);
2707        for db in &databases {
2708            result.push_str(&format!("  {}\n", db.schema_summary()));
2709        }
2710
2711        info!("  ✓ Listed {} database(s)", db_count);
2712        ToolResult::success("list_databases", db_count.to_string(), result)
2713    }
2714}