Skip to main content

netget/llm/actions/
common.rs

1//! Common actions available in all prompts
2//!
3//! This module defines actions that are available in both user input
4//! and network event prompts (show_message, memory operations, etc.).
5
6use super::{ActionDefinition, Parameter};
7use anyhow::{Context, Result};
8use serde::{Deserialize, Serialize};
9use serde_json::json;
10
11/// Flexible deserializers for numeric types that accept both numbers and strings
12mod flexible_deserializers {
13    use serde::{Deserialize, Deserializer};
14
15    /// Deserialize u32 from either a number or a string
16    pub fn deserialize_u32_flexible<'de, D>(deserializer: D) -> Result<u32, D::Error>
17    where
18        D: Deserializer<'de>,
19    {
20        #[derive(Deserialize)]
21        #[serde(untagged)]
22        enum U32OrString {
23            Number(u32),
24            String(String),
25        }
26
27        match U32OrString::deserialize(deserializer)? {
28            U32OrString::Number(n) => Ok(n),
29            U32OrString::String(s) => s.parse().map_err(serde::de::Error::custom),
30        }
31    }
32
33    /// Deserialize Option<u32> from either a number or a string
34    pub fn deserialize_option_u32_flexible<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
35    where
36        D: Deserializer<'de>,
37    {
38        #[derive(Deserialize)]
39        #[serde(untagged)]
40        enum U32OrString {
41            Number(u32),
42            String(String),
43        }
44
45        match Option::<U32OrString>::deserialize(deserializer)? {
46            Some(U32OrString::Number(n)) => Ok(Some(n)),
47            Some(U32OrString::String(s)) => s.parse().map(Some).map_err(serde::de::Error::custom),
48            None => Ok(None),
49        }
50    }
51
52    /// Deserialize u64 from either a number or a string
53    #[allow(dead_code)]
54    pub fn deserialize_u64_flexible<'de, D>(deserializer: D) -> Result<u64, D::Error>
55    where
56        D: Deserializer<'de>,
57    {
58        #[derive(Deserialize)]
59        #[serde(untagged)]
60        enum U64OrString {
61            Number(u64),
62            String(String),
63        }
64
65        match U64OrString::deserialize(deserializer)? {
66            U64OrString::Number(n) => Ok(n),
67            U64OrString::String(s) => s.parse().map_err(serde::de::Error::custom),
68        }
69    }
70
71    /// Deserialize Option<u64> from either a number or a string
72    pub fn deserialize_option_u64_flexible<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
73    where
74        D: Deserializer<'de>,
75    {
76        #[derive(Deserialize)]
77        #[serde(untagged)]
78        enum U64OrString {
79            Number(u64),
80            String(String),
81        }
82
83        match Option::<U64OrString>::deserialize(deserializer)? {
84            Some(U64OrString::Number(n)) => Ok(Some(n)),
85            Some(U64OrString::String(s)) => s.parse().map(Some).map_err(serde::de::Error::custom),
86            None => Ok(None),
87        }
88    }
89
90    /// Deserialize u16 from either a number or a string
91    #[allow(dead_code)]
92    pub fn deserialize_u16_flexible<'de, D>(deserializer: D) -> Result<u16, D::Error>
93    where
94        D: Deserializer<'de>,
95    {
96        #[derive(Deserialize)]
97        #[serde(untagged)]
98        enum U16OrString {
99            Number(u16),
100            String(String),
101        }
102
103        match U16OrString::deserialize(deserializer)? {
104            U16OrString::Number(n) => Ok(n),
105            U16OrString::String(s) => s.parse().map_err(serde::de::Error::custom),
106        }
107    }
108
109    /// Deserialize Option<u16> from either a number or a string
110    pub fn deserialize_option_u16_flexible<'de, D>(deserializer: D) -> Result<Option<u16>, D::Error>
111    where
112        D: Deserializer<'de>,
113    {
114        #[derive(Deserialize)]
115        #[serde(untagged)]
116        enum U16OrString {
117            Number(u16),
118            String(String),
119        }
120
121        match Option::<U16OrString>::deserialize(deserializer)? {
122            Some(U16OrString::Number(n)) => Ok(Some(n)),
123            Some(U16OrString::String(s)) => s.parse().map(Some).map_err(serde::de::Error::custom),
124            None => Ok(None),
125        }
126    }
127}
128
129/// Type alias for protocol groups mapping
130type ProtocolGroups = std::collections::HashMap<
131    &'static str,
132    Vec<(String, std::sync::Arc<dyn crate::llm::actions::Server>)>,
133>;
134
135/// Task definition for attaching to a server at creation time
136#[derive(Debug, Clone, Deserialize, Serialize)]
137pub struct ServerTaskDefinition {
138    pub task_id: String,
139    pub recurring: bool,
140    #[serde(
141        default,
142        deserialize_with = "flexible_deserializers::deserialize_option_u64_flexible"
143    )]
144    pub delay_secs: Option<u64>,
145    #[serde(
146        default,
147        deserialize_with = "flexible_deserializers::deserialize_option_u64_flexible"
148    )]
149    pub interval_secs: Option<u64>,
150    #[serde(
151        default,
152        deserialize_with = "flexible_deserializers::deserialize_option_u64_flexible"
153    )]
154    pub max_executions: Option<u64>,
155    pub instruction: String,
156    #[serde(default)]
157    pub context: Option<serde_json::Value>,
158}
159
160/// Common actions available in all contexts
161#[derive(Debug, Clone, Deserialize, Serialize)]
162#[serde(tag = "type", rename_all = "snake_case")]
163pub enum CommonAction {
164    /// Display a message to the user
165    ShowMessage { message: String },
166
167    /// Open a new server
168    OpenServer {
169        // === NEW FLEXIBLE BINDING FIELDS ===
170        /// MAC address (for layer 2 protocols like ARP spoofing)
171        #[serde(default, skip_serializing_if = "Option::is_none")]
172        mac_address: Option<String>,
173        /// Network interface to bind (for raw protocols like ICMP, ARP, DataLink)
174        #[serde(default, skip_serializing_if = "Option::is_none")]
175        interface: Option<String>,
176        /// Host address (hostname or IP) to bind (IPv4, IPv6, or hostname)
177        #[serde(default, skip_serializing_if = "Option::is_none")]
178        host: Option<String>,
179        /// Port to bind (for socket-based protocols like TCP, HTTP, DNS)
180        #[serde(
181            default,
182            skip_serializing_if = "Option::is_none",
183            deserialize_with = "flexible_deserializers::deserialize_option_u16_flexible"
184        )]
185        port: Option<u16>,
186
187        protocol: String,
188        #[serde(default)]
189        send_first: bool,
190        #[serde(default)]
191        initial_memory: Option<String>,
192        /// Server instruction for LLM (optional - only required if not using event handlers)
193        #[serde(default)]
194        instruction: String,
195        #[serde(default)]
196        startup_params: Option<serde_json::Value>,
197        // Event handler configuration
198        #[serde(default)]
199        event_handlers: Option<Vec<serde_json::Value>>,
200        // Scheduled tasks to create with this server
201        #[serde(default)]
202        scheduled_tasks: Option<Vec<ServerTaskDefinition>>,
203        // Feedback instructions for automatic server adjustment
204        #[serde(default)]
205        feedback_instructions: Option<String>,
206    },
207
208    /// Close a specific server
209    CloseServer {
210        #[serde(deserialize_with = "flexible_deserializers::deserialize_u32_flexible")]
211        server_id: u32,
212    },
213
214    /// Close all servers
215    CloseAllServers,
216
217    /// Open a new client connection
218    OpenClient {
219        protocol: String,
220        remote_addr: String,
221        /// Client instruction for LLM (optional - only required if not using event handlers)
222        #[serde(default)]
223        instruction: String,
224        #[serde(default)]
225        startup_params: Option<serde_json::Value>,
226        #[serde(default)]
227        initial_memory: Option<String>,
228        // Event handler configuration
229        #[serde(default)]
230        event_handlers: Option<Vec<serde_json::Value>>,
231        // Scheduled tasks to create with this client
232        #[serde(default)]
233        scheduled_tasks: Option<Vec<ServerTaskDefinition>>,
234        // Feedback instructions for automatic client adjustment
235        #[serde(default)]
236        feedback_instructions: Option<String>,
237    },
238
239    /// Close a specific client
240    CloseClient {
241        #[serde(deserialize_with = "flexible_deserializers::deserialize_u32_flexible")]
242        client_id: u32,
243    },
244
245    /// Close all clients
246    CloseAllClients,
247
248    /// Close a specific connection by its unified ID
249    CloseConnectionById {
250        #[serde(deserialize_with = "flexible_deserializers::deserialize_u32_flexible")]
251        connection_id: u32,
252    },
253
254    /// Reconnect a disconnected client
255    ReconnectClient {
256        #[serde(deserialize_with = "flexible_deserializers::deserialize_u32_flexible")]
257        client_id: u32,
258    },
259
260    /// Update the client instruction
261    UpdateClientInstruction {
262        #[serde(deserialize_with = "flexible_deserializers::deserialize_u32_flexible")]
263        client_id: u32,
264        instruction: String,
265    },
266
267    /// Update a running server in place, by id. Only the fields supplied are
268    /// changed. Binding fields (port/host/interface/mac_address) or a
269    /// startup_params change trigger a clean stop+start; every other field is
270    /// applied without dropping connections. Fixes the "spawn a second server
271    /// instead of updating" problem by making update a first-class operation.
272    UpdateServer {
273        #[serde(deserialize_with = "flexible_deserializers::deserialize_u32_flexible")]
274        server_id: u32,
275        #[serde(
276            default,
277            deserialize_with = "flexible_deserializers::deserialize_option_u16_flexible"
278        )]
279        port: Option<u16>,
280        #[serde(default)]
281        host: Option<String>,
282        #[serde(default)]
283        interface: Option<String>,
284        #[serde(default)]
285        mac_address: Option<String>,
286        #[serde(default)]
287        send_first: bool,
288        #[serde(default)]
289        instruction: Option<String>,
290        #[serde(default, alias = "memory")]
291        initial_memory: Option<String>,
292        #[serde(default)]
293        startup_params: Option<serde_json::Value>,
294        #[serde(default)]
295        event_handlers: Option<Vec<serde_json::Value>>,
296        #[serde(default)]
297        scheduled_tasks: Option<Vec<ServerTaskDefinition>>,
298        #[serde(default)]
299        feedback_instructions: Option<String>,
300    },
301
302    /// Update a running client in place, by id. Mirror of `update_server`: a
303    /// remote_addr or startup_params change reconnects, everything else applies
304    /// in place.
305    UpdateClient {
306        #[serde(deserialize_with = "flexible_deserializers::deserialize_u32_flexible")]
307        client_id: u32,
308        #[serde(default)]
309        remote_addr: Option<String>,
310        #[serde(default)]
311        instruction: Option<String>,
312        #[serde(default, alias = "memory")]
313        initial_memory: Option<String>,
314        #[serde(default)]
315        startup_params: Option<serde_json::Value>,
316        #[serde(default)]
317        event_handlers: Option<Vec<serde_json::Value>>,
318        #[serde(default)]
319        scheduled_tasks: Option<Vec<ServerTaskDefinition>>,
320        #[serde(default)]
321        feedback_instructions: Option<String>,
322    },
323
324    /// Update the server instruction (combines with existing)
325    UpdateInstruction { instruction: String },
326
327    /// Change the LLM model
328    ChangeModel { model: String },
329
330    /// Replace global memory completely
331    SetMemory { value: String },
332
333    /// Append to global memory
334    AppendMemory { value: String },
335
336    /// Append content to a log file
337    AppendToLog {
338        output_name: String,
339        content: String,
340    },
341
342    /// Schedule a task (one-shot or recurring)
343    ScheduleTask {
344        task_id: String,
345        recurring: bool,
346        #[serde(
347            default,
348            deserialize_with = "flexible_deserializers::deserialize_option_u64_flexible"
349        )]
350        delay_secs: Option<u64>,
351        #[serde(
352            default,
353            deserialize_with = "flexible_deserializers::deserialize_option_u64_flexible"
354        )]
355        interval_secs: Option<u64>,
356        #[serde(
357            default,
358            deserialize_with = "flexible_deserializers::deserialize_option_u64_flexible"
359        )]
360        max_executions: Option<u64>,
361        #[serde(
362            default,
363            deserialize_with = "flexible_deserializers::deserialize_option_u32_flexible"
364        )]
365        server_id: Option<u32>,
366        #[serde(default)]
367        connection_id: Option<String>,
368        #[serde(
369            default,
370            deserialize_with = "flexible_deserializers::deserialize_option_u32_flexible"
371        )]
372        client_id: Option<u32>,
373        instruction: String,
374        #[serde(default)]
375        context: Option<serde_json::Value>,
376        // Script configuration fields
377        #[serde(default)]
378        script_runtime: Option<String>,
379        #[serde(default)]
380        script_language: Option<String>,
381        #[serde(default)]
382        script_path: Option<String>,
383        #[serde(default)]
384        script_inline: Option<String>,
385        #[serde(default)]
386        script_handles: Option<Vec<String>>,
387    },
388
389    /// Cancel a scheduled task
390    CancelTask { task_id: String },
391
392    /// Provide feedback for automatic server/client adjustment
393    /// This action accumulates feedback for later LLM processing (if feedback_instructions is set)
394    ProvideFeedback {
395        /// Feedback data (free-form JSON describing what to learn/adjust)
396        feedback: serde_json::Value,
397    },
398
399    /// Create a new SQLite database (file-based or in-memory)
400    #[cfg(feature = "sqlite")]
401    CreateDatabase {
402        name: String,
403        #[serde(default)]
404        is_memory: bool, // true = in-memory (:memory:), false = file-based (./netget_db_<name>.db)
405        #[serde(default)]
406        owner: Option<String>, // "server-N", "client-N", or "global"
407        #[serde(default)]
408        schema_ddl: Option<String>, // Initial DDL statements to create tables
409    },
410
411    /// Delete a database
412    #[cfg(feature = "sqlite")]
413    DeleteDatabase { database_id: u32 },
414}
415
416impl CommonAction {
417    /// Parse from JSON value
418    pub fn from_json(value: &serde_json::Value) -> Result<Self> {
419        // BACKWARD COMPATIBILITY: Handle old field names
420        let mut value_mut = value.clone();
421        if let Some(obj) = value_mut.as_object_mut() {
422            // TOLERANCE: some models nest parameters under args/arguments/parameters
423            // (OpenAI tool-call style) instead of placing them flat on the object.
424            // Flatten them so {"type":"open_server","args":{"port":8123}} parses.
425            for wrapper in ["args", "arguments", "parameters"] {
426                if let Some(serde_json::Value::Object(inner)) = obj.remove(wrapper) {
427                    for (k, v) in inner {
428                        obj.entry(k).or_insert(v);
429                    }
430                }
431            }
432        }
433        if let Some(obj) = value_mut.as_object_mut() {
434            // BACKWARD COMPATIBILITY: Rename base_stack to protocol (if protocol doesn't exist)
435            if matches!(
436                obj.get("type").and_then(|v| v.as_str()),
437                Some("open_server") | Some("open_client")
438            ) {
439                if let Some(base_stack) = obj.remove("base_stack") {
440                    // Only set protocol if it doesn't already exist
441                    if !obj.contains_key("protocol") {
442                        obj.insert("protocol".to_string(), base_stack);
443                    }
444                }
445            }
446
447            // BACKWARD COMPATIBILITY: Convert old script_inline/script_handles format to new event_handlers format
448            // Check if this is an open_server or open_client action with old script fields
449            if matches!(
450                obj.get("type").and_then(|v| v.as_str()),
451                Some("open_server") | Some("open_client")
452            ) {
453                if let (Some(script_inline), Some(script_handles)) = (
454                    obj.get("script_inline").and_then(|v| v.as_str()),
455                    obj.get("script_handles").and_then(|v| v.as_array()),
456                ) {
457                    // Convert to new event_handlers format
458                    let mut event_handlers = Vec::new();
459
460                    // Create a handler for each event type in script_handles
461                    for event_type in script_handles {
462                        if let Some(event_type_str) = event_type.as_str() {
463                            event_handlers.push(serde_json::json!({
464                                "event_pattern": event_type_str,
465                                "handler": {
466                                    "type": "script",
467                                    "language": "python",
468                                    "code": script_inline
469                                }
470                            }));
471                        }
472                    }
473
474                    // Add event_handlers field if we created any
475                    if !event_handlers.is_empty() {
476                        obj.insert(
477                            "event_handlers".to_string(),
478                            serde_json::json!(event_handlers),
479                        );
480                    }
481
482                    // Remove old fields
483                    obj.remove("script_inline");
484                    obj.remove("script_handles");
485                }
486            }
487        }
488
489        serde_json::from_value(value_mut).context("Failed to parse common action")
490    }
491}
492
493/// Get action definition for show_message
494pub fn show_message_action() -> ActionDefinition {
495    ActionDefinition {
496        name: "show_message".to_string(),
497        description: "Display a message to the user controlling NetGet".to_string(),
498        parameters: vec![Parameter {
499            name: "message".to_string(),
500            type_hint: "string".to_string(),
501            description: "Message to display".to_string(),
502            required: true,
503        }],
504        example: json!({
505            "type": "show_message",
506            "message": "Server started successfully on port 8080"
507        }),
508        log_template: None,
509    }
510}
511
512/// Get action definition for open_server
513pub fn open_server_action(
514    _selected_mode: crate::state::app_state::ScriptingMode,
515    env: &crate::scripting::ScriptingEnvironment,
516    is_enabled: bool,
517) -> ActionDefinition {
518    let name = "open_server".to_string();
519    let mut description = "Start a new server.\n\n\
520        PARAMETER USAGE RULES:\n\
521        1. ONLY use parameters that are explicitly documented below\n\
522        2. DO NOT invent new parameters, even if they seem logical\n\
523        3. For custom requirements (timeouts, special behavior, etc.):\n\
524           - Put them in the 'instruction' field as natural language\n\n\
525        EXAMPLE - User says 'open HTTP server with 30 second timeout':\n\
526        ❌ WRONG: {\"type\": \"open_server\", \"protocol\": \"http\", \"timeout\": 30}\n\
527        ✅ RIGHT: {\"type\": \"open_server\", \"protocol\": \"http\", \"instruction\": \"HTTP server with 30 second timeout\"}\n\n\
528        TASK SCHEDULING RULES:\n\
529        FOR PERIODIC TASKS (heartbeat, every X seconds/minutes):\n\
530        - Use 'scheduled_tasks' parameter with interval_secs\n\
531        - DO NOT use event_handlers for time-based tasks\n\n\
532        EXAMPLE - User says 'send heartbeat every 10 seconds':\n\
533        ❌ WRONG: {\"event_handlers\": [{\"event_pattern\": \"*\", \"handler\": {...}}]}\n\
534        ✅ RIGHT: {\"scheduled_tasks\": [{\"task_id\": \"heartbeat\", \"recurring\": true, \"interval_secs\": 10, \"instruction\": \"Send heartbeat log\"}]}\n\n\
535        FOR NETWORK EVENTS (data received, connection made):\n\
536        - Use 'event_handlers' parameter\n\
537        - Only for responding to actual network events".to_string();
538
539    if !is_enabled {
540        description.push_str(" ⚠️ DISABLED: You must first read protocol documentation using the read_documentation tool. This tool lists all available protocols (both server and client) and provides detailed configuration information including when to use server vs client mode.");
541        return ActionDefinition {
542            name,
543            description,
544            parameters: vec![],
545            example: json!({}),
546            log_template: None,
547        };
548    }
549
550    let mut parameters = vec![
551            Parameter {
552                name: "mac_address".to_string(),
553                type_hint: "string".to_string(),
554                description: "Optional: MAC address for Layer 2 protocols (e.g., ARP spoofing). Format: \"00:11:22:33:44:55\". Most protocols don't need this.".to_string(),
555                required: false,
556            },
557            Parameter {
558                name: "interface".to_string(),
559                type_hint: "string".to_string(),
560                description: "Optional: Network interface to bind (for raw protocols like ICMP, ARP, DataLink). \
561                    Common interface names: \"lo\" or \"lo0\" (loopback), \"eth0\" or \"en0\" (Ethernet), \"wlan0\" (WiFi). \
562                    NOTE: Only specify if the protocol specifically requires it (e.g., DataLink). Most port-based protocols (TCP, HTTP, DNS) don't use this. \
563                    If you need to discover available interfaces, you can try common names like \"lo\" for loopback or use the system's default interface by omitting this parameter.".to_string(),
564                required: false,
565            },
566            Parameter {
567                name: "host".to_string(),
568                type_hint: "string".to_string(),
569                description: "Optional: Host address to bind (IPv4, IPv6, or hostname). Examples: \"127.0.0.1\" (loopback), \"0.0.0.0\" (all interfaces), \"::\". Protocols will use sensible defaults if omitted.".to_string(),
570                required: false,
571            },
572            Parameter {
573                name: "port".to_string(),
574                type_hint: "number".to_string(),
575                description: "Optional: Port number to listen on. Use 0 to automatically find an available port. Required for port-based protocols (TCP, HTTP, DNS). Raw protocols (ICMP, ARP) don't use this.".to_string(),
576                required: false,
577            },
578            Parameter {
579                name: "protocol".to_string(),
580                type_hint: "string".to_string(),
581                description: format!(
582                    "Protocol to use. ALWAYS prefer high-level protocols when user keywords match: \
583                    if user says 'dns' or 'dns server' → use 'dns' (NOT 'udp'), \
584                    if user says 'http' or 'web server' → use 'http' (NOT 'tcp'), \
585                    if user says 'smtp' or 'mail server' → use 'smtp' (NOT 'tcp'). \
586                    Only use low-level protocols (tcp, udp) for custom protocols without a specific high-level match. \
587                    Available: {}",
588                    all_base_stacks(false, None).join(", ")
589                ),
590                required: true,
591            },
592            Parameter {
593                name: "send_first".to_string(),
594                type_hint: "boolean".to_string(),
595                description: "True if server sends data first (FTP, SMTP), false if it waits for client (HTTP)".to_string(),
596                required: false,
597            },
598            Parameter {
599                name: "initial_memory".to_string(),
600                type_hint: "string".to_string(),
601                description: "Optional initial memory as a string. Use for storing persistent context across connections. Example: \"user_count: 0\"".to_string(),
602                required: false,
603            },
604            Parameter {
605                name: "instruction".to_string(),
606                type_hint: "string".to_string(),
607                description: "Detailed instructions for handling network events. \
608                    Use this field for custom requirements that don't have dedicated parameters \
609                    (e.g., 'with 30 second timeout', 'log all requests to file', 'rate limit to 10 requests per second', etc.)".to_string(),
610                required: true,
611            },
612            Parameter {
613                name: "startup_params".to_string(),
614                type_hint: "object".to_string(),
615                description: "Optional protocol-specific startup parameters. ONLY the \
616                              parameters the protocol's documentation declares are accepted: an \
617                              undeclared key is refused by name and THE SERVER DOES NOT START. \
618                              A protocol that declares none takes no startup_params at all. \
619                              Anything the protocol does not declare - what the server should \
620                              answer, which mailbox or database exists, what data to serve - \
621                              belongs in 'instruction', not here."
622                    .to_string(),
623                required: false,
624            },
625            Parameter {
626                name: "scheduled_tasks".to_string(),
627                type_hint: "array".to_string(),
628                description: "Optional: Array of TIME-BASED tasks that execute periodically or after a delay. \
629                    \
630                    USE WHEN: User says 'every X seconds/minutes', 'heartbeat', 'periodic', 'scheduled', or describes time-based automation. \
631                    \
632                    EXAMPLES: \
633                    - 'send heartbeat every 10 seconds' → scheduled_tasks with interval_secs: 10 \
634                    - 'check status every minute' → scheduled_tasks with interval_secs: 60 \
635                    - 'cleanup after 30 seconds' → scheduled_tasks with delay_secs: 30 \
636                    \
637                    DO NOT use event_handlers for periodic tasks - event_handlers respond to network events, NOT time-based triggers! \
638                    \
639                    Each task has: task_id (string), recurring (boolean), interval_secs (for periodic) OR delay_secs (for one-shot), max_executions (optional), instruction (what to do), context (optional).".to_string(),
640                required: false,
641            },
642        ];
643
644    // Add event_handlers parameter with description based on handler mode
645    let handler_mode_guidance = "You can configure different handlers for different events. Each handler specifies an event_pattern (specific event ID or \"*\" for all events) and a handler type (script, static, or llm). Handlers are matched in order - first match wins.";
646
647    let available_runtimes = env.format_available();
648    let event_handlers_description = format!(
649        "Optional: Array of event handlers to configure how events are processed. {}\\n\\n\
650Each handler has:\\n\
651- event_pattern: Event ID to match (e.g., \\\"tcp_data_received\\\") or \\\"*\\\" for all events\\n\
652- handler: Object with:\\n\
653  - type: \\\"script\\\" (inline code), \\\"static\\\" (predefined actions), or \\\"llm\\\" (dynamic processing)\\n\\n\
654REQUIRED FIELDS BY TYPE:\\n\
655  - For script: language ({}), code (inline script)\\n\
656  - For static: actions (array of action objects)\\n\
657  - For llm: instruction (string, REQUIRED) - describes how the LLM should handle this event\\n\\n\
658CRITICAL: LLM handlers MUST include 'instruction' field. Example: {{\\\"type\\\": \\\"llm\\\", \\\"instruction\\\": \\\"Handle HTTP requests...\\\"}}\\n\\n\
659SCRIPT EVENT DATA STRUCTURE:\\n\
660Scripts receive JSON via stdin with this structure:\\n\
661{{\\n\
662  \\\"event_type_id\\\": \\\"http_request\\\",  // Event type identifier\\n\
663  \\\"server\\\": {{\\\"id\\\": 1, \\\"port\\\": 8080, \\\"stack\\\": \\\"HTTP\\\", \\\"memory\\\": \\\"\\\", \\\"instruction\\\": \\\"...\\\"}},\\n\
664  \\\"connection\\\": {{\\\"id\\\": \\\"1\\\", \\\"remote_addr\\\": \\\"127.0.0.1:12345\\\"}},  // Optional\\n\
665  \\\"event\\\": {{\\n\
666    // Protocol-specific event data (fields vary by event type)\\n\
667    // For HTTP: method, path, query_string, query, headers, body\\n\
668    // For TCP: data (hex-encoded bytes)\\n\
669    // For DNS: query_id, domain, query_type\\n\
670  }}\\n\
671}}\\n\\n\
672IMPORTANT: Event data is directly under data['event'], NOT data['event']['data']!\\n\
673Access pattern: data['event']['field_name'] (e.g., data['event']['method'])\\n\\n\
674CRITICAL - COMMON MISTAKES TO AVOID:\\n\
675❌ WRONG: data['event']['request']['query_string']      # NO 'request' wrapper!\\n\
676❌ WRONG: data['event']['http_request']['query_string'] # NO 'http_request' wrapper!\\n\
677❌ WRONG: data['event']['data']['method']               # NO 'data' wrapper!\\n\
678✅ RIGHT: data['event']['query_string']                 # Direct access\\n\
679✅ RIGHT: data['event']['method']                       # Direct access\\n\\n\
680The event_type_id tells you WHAT event occurred, but data fields are DIRECTLY under data['event'].\\n\\n\
681Example HTTP script (sum query parameters x and y):\\n\
682{{\\\"event_pattern\\\": \\\"http_request\\\", \\\"handler\\\": {{\\\"type\\\": \\\"script\\\", \\\"language\\\": \\\"python\\\", \\\"code\\\": \\\"<http_sum_script>\\\"}}}}\\n\\n\
683<http_sum_script>\\n\
684import json\\n\
685import sys\\n\
686\\n\
687data = json.load(sys.stdin)\\n\
688# Access event data: data['event']['field_name']\\n\
689query_params = data['event']['query']  # Pre-parsed query parameters object\\n\
690x = float(query_params['x'])\\n\
691y = float(query_params['y'])\\n\
692result = x + y\\n\
693\\n\
694print(json.dumps({{\\n\
695  'actions': [{{\\n\
696    'type': 'send_http_response',\\n\
697    'status': 200,\\n\
698    'body': str(result)\\n\
699  }}]\\n\
700}}))\\n\
701</http_sum_script>\\n\\n\
702Example TCP script (echo received data):\\n\
703{{\\\"event_pattern\\\": \\\"tcp_data_received\\\", \\\"handler\\\": {{\\\"type\\\": \\\"script\\\", \\\"language\\\": \\\"python\\\", \\\"code\\\": \\\"<tcp_echo_script>\\\"}}}}\\n\\n\
704<tcp_echo_script>\\n\
705import json\\n\
706import sys\\n\
707\\n\
708data = json.load(sys.stdin)\\n\
709# TCP data is hex-encoded in data['event']['data']\\n\
710received_hex = data['event']['data']\\n\
711\\n\
712print(json.dumps({{\\n\
713  'actions': [{{\\n\
714    'type': 'send_tcp_data',\\n\
715    'data': received_hex  # Echo back the same hex data\\n\
716  }}]\\n\
717}}))\\n\
718</tcp_echo_script>\\n\\n\
719Example static handler:\\n\
720{{\\\"event_pattern\\\": \\\"*\\\", \\\"handler\\\": {{\\\"type\\\": \\\"static\\\", \\\"actions\\\": [{{\\\"type\\\": \\\"send_data\\\", \\\"data\\\": \\\"Welcome\\\"}}]}}}}\\n\\n\
721Example LLM handler:\\n\
722{{\\\"event_pattern\\\": \\\"http_request\\\", \\\"handler\\\": {{\\\"type\\\": \\\"llm\\\", \\\"instruction\\\": \\\"You are a recipe website\\\"}}}}",
723        handler_mode_guidance,
724        available_runtimes
725    );
726
727    parameters.push(Parameter {
728        name: "event_handlers".to_string(),
729        type_hint: "array".to_string(),
730        description: event_handlers_description,
731        required: false,
732    });
733
734    parameters.push(Parameter {
735        name: "feedback_instructions".to_string(),
736        type_hint: "string".to_string(),
737        description: "Optional: Instructions for automatic server adjustment based on network request feedback. When set, network requests can provide feedback via the 'provide_feedback' action. Feedback is accumulated and debounced (leading edge), then the LLM is invoked with these instructions to decide how to adjust the server behavior (e.g., update instructions, modify handlers, change configuration). Example: \"Adjust response time if clients are timing out\" or \"Learn from failed requests and improve error handling\".".to_string(),
738        required: false,
739    });
740
741    let example = json!({
742        "type": "open_server",
743        "port": 21,
744        "protocol": "tcp",
745        "send_first": true,
746        "initial_memory": "login_count: 0\nfiles: data.txt,readme.md",
747        "instruction": "You are an FTP server. Respond to FTP commands like USER, PASS, LIST, RETR, QUIT with appropriate FTP response codes."
748    });
749
750    ActionDefinition {
751        name,
752        description,
753        parameters,
754        example,
755        log_template: None,
756    }
757}
758
759/// Get action definition for close_server
760pub fn close_server_action() -> ActionDefinition {
761    ActionDefinition {
762        name: "close_server".to_string(),
763        description: "Stop a specific server by ID.".to_string(),
764        parameters: vec![Parameter {
765            name: "server_id".to_string(),
766            type_hint: "number".to_string(),
767            description: "Server ID to close (e.g., 1, 2).".to_string(),
768            required: true,
769        }],
770        example: json!({
771            "type": "close_server",
772            "server_id": 1
773        }),
774        log_template: None,
775    }
776}
777
778/// Get action definition for close_all_servers
779pub fn close_all_servers_action() -> ActionDefinition {
780    ActionDefinition {
781        name: "close_all_servers".to_string(),
782        description: "Stop all running servers.".to_string(),
783        parameters: vec![],
784        example: json!({
785            "type": "close_all_servers"
786        }),
787        log_template: None,
788    }
789}
790
791/// Get action definition for open_client
792pub fn open_client_action(
793    _selected_mode: crate::state::app_state::ScriptingMode,
794    env: &crate::scripting::ScriptingEnvironment,
795    is_enabled: bool,
796) -> ActionDefinition {
797    let name = "open_client".to_string();
798    let mut description = "Connect to a remote server as a client.".to_string();
799
800    if !is_enabled {
801        description.push_str(" ⚠️ DISABLED: You must first read protocol documentation using the read_documentation tool. This tool lists all available protocols (both server and client) and provides detailed configuration information including when to use server vs client mode.");
802        return ActionDefinition {
803            name,
804            description,
805            parameters: vec![],
806            example: serde_json::Value::Null,
807            log_template: None,
808        };
809    }
810
811    let mut parameters = vec![
812        Parameter {
813            name: "protocol".to_string(),
814            type_hint: "string".to_string(),
815            description: "Protocol to use for connection (e.g., 'tcp', 'http', 'redis', 'ssh')".to_string(),
816            required: true,
817        },
818        Parameter {
819            name: "remote_addr".to_string(),
820            type_hint: "string".to_string(),
821            description: "Remote server address as 'hostname:port' or 'IP:port' (e.g., 'example.com:80', '192.168.1.1:6379', 'localhost:8080')".to_string(),
822            required: true,
823        },
824        Parameter {
825            name: "instruction".to_string(),
826            type_hint: "string".to_string(),
827            description: "Detailed instructions for controlling the client (how to send data, interpret responses, make decisions)".to_string(),
828            required: true,
829        },
830        Parameter {
831            name: "initial_memory".to_string(),
832            type_hint: "string".to_string(),
833            description: "Optional initial memory as a string. Use for storing persistent context. Example: \"auth_token: abc123\\nrequest_count: 0\"".to_string(),
834            required: false,
835        },
836        Parameter {
837            name: "startup_params".to_string(),
838            type_hint: "object".to_string(),
839            description: "Optional protocol-specific startup parameters. ONLY the parameters \
840                          the protocol's documentation declares are accepted; an undeclared key \
841                          is refused by name and the client does not connect. Behavioural \
842                          detail belongs in 'instruction', not here. For example, HTTP clients \
843                          may accept default headers or user agent settings."
844                .to_string(),
845            required: false,
846        },
847        Parameter {
848            name: "scheduled_tasks".to_string(),
849            type_hint: "array".to_string(),
850            description: "Optional: Array of scheduled tasks to create with this client. Each task will be attached to the client and execute at specified intervals or delays. Tasks are automatically cleaned up when the client disconnects.".to_string(),
851            required: false,
852        },
853    ];
854
855    // Add event_handlers parameter
856    let handler_mode_guidance = "You can configure different handlers for different client events. Each handler specifies an event_pattern (specific event ID or \"*\" for all events) and a handler type (script, static, or llm). Handlers are matched in order - first match wins.";
857
858    let available_runtimes = env.format_available();
859    let event_handlers_description = format!(
860        "Optional: Array of event handlers to configure how client events are processed. {}\\n\\n\
861Each handler has:\\n\
862- event_pattern: Event ID to match (e.g., \\\"http_response_received\\\") or \\\"*\\\" for all events\\n\
863- handler: Object with:\\n\
864  - type: \\\"script\\\" (inline code), \\\"static\\\" (predefined actions), or \\\"llm\\\" (dynamic processing)\\n\
865  - For script: language ({}), code (inline script)\\n\
866  - For static: actions (array of action objects)\\n\
867  - For llm: instruction (REQUIRED - describes how the LLM should handle this event)\\n\\n\
868Note: Client scripts use the same event data structure as server scripts (see open_server documentation for details).\\n\
869Access pattern: data['event']['field_name'] (e.g., data['event']['status_code'] for HTTP responses)\\n\\n\
870Example script handler: {{\\\"event_pattern\\\": \\\"redis_response_received\\\", \\\"handler\\\": {{\\\"type\\\": \\\"script\\\", \\\"language\\\": \\\"python\\\", \\\"code\\\": \\\"import json,sys;data=json.load(sys.stdin);print(json.dumps({{'actions':[{{'type':'execute_redis_command','command':'PING'}}]}}))\\\"}}}}\\n\\n\
871Example static handler: {{\\\"event_pattern\\\": \\\"*\\\", \\\"handler\\\": {{\\\"type\\\": \\\"static\\\", \\\"actions\\\": [{{\\\"type\\\": \\\"send_http_request\\\", \\\"method\\\": \\\"GET\\\", \\\"path\\\": \\\"/\\\"}}]}}}}\\n\\n\
872Example LLM handler: {{\\\"event_pattern\\\": \\\"http_response_received\\\", \\\"handler\\\": {{\\\"type\\\": \\\"llm\\\", \\\"instruction\\\": \\\"You are a recipe website\\\"}}}}",
873        handler_mode_guidance,
874        available_runtimes
875    );
876
877    parameters.push(Parameter {
878        name: "event_handlers".to_string(),
879        type_hint: "array".to_string(),
880        description: event_handlers_description,
881        required: false,
882    });
883
884    parameters.push(Parameter {
885        name: "feedback_instructions".to_string(),
886        type_hint: "string".to_string(),
887        description: "Optional: Instructions for automatic client adjustment based on server response feedback. When set, server responses can provide feedback via the 'provide_feedback' action. Feedback is accumulated and debounced (leading edge), then the LLM is invoked with these instructions to decide how to adjust the client behavior (e.g., update request strategy, modify retry logic, change authentication method). Example: \"Adjust request rate if server is throttling\" or \"Learn from error responses and modify request format\".".to_string(),
888        required: false,
889    });
890
891    let example = json!({
892        "type": "open_client",
893        "protocol": "http",
894        "remote_addr": "example.com:80",
895        "instruction": "Send a GET request to /api/status and log the response code."
896    });
897
898    ActionDefinition {
899        name,
900        description,
901        parameters,
902        example,
903        log_template: None,
904    }
905}
906
907/// Get action definition for close_client
908pub fn close_client_action() -> ActionDefinition {
909    ActionDefinition {
910        name: "close_client".to_string(),
911        description: "Disconnect a specific client by ID.".to_string(),
912        parameters: vec![Parameter {
913            name: "client_id".to_string(),
914            type_hint: "number".to_string(),
915            description: "Client ID to close (e.g., 1, 2).".to_string(),
916            required: true,
917        }],
918        example: json!({
919            "type": "close_client",
920            "client_id": 1
921        }),
922        log_template: None,
923    }
924}
925
926/// Get action definition for close_all_clients
927pub fn close_all_clients_action() -> ActionDefinition {
928    ActionDefinition {
929        name: "close_all_clients".to_string(),
930        description: "Disconnect all active clients.".to_string(),
931        parameters: vec![],
932        example: json!({
933            "type": "close_all_clients"
934        }),
935        log_template: None,
936    }
937}
938
939/// Get action definition for close_connection_by_id
940pub fn close_connection_by_id_action() -> ActionDefinition {
941    ActionDefinition {
942        name: "close_connection_by_id".to_string(),
943        description: "Close a specific connection by its unified ID.".to_string(),
944        parameters: vec![Parameter {
945            name: "connection_id".to_string(),
946            type_hint: "number".to_string(),
947            description: "Unified ID of the connection to close (e.g., 3, 5).".to_string(),
948            required: true,
949        }],
950        example: json!({
951            "type": "close_connection_by_id",
952            "connection_id": 3
953        }),
954        log_template: None,
955    }
956}
957
958/// Get action definition for reconnect_client
959pub fn reconnect_client_action() -> ActionDefinition {
960    ActionDefinition {
961        name: "reconnect_client".to_string(),
962        description: "Reconnect a disconnected client to its remote server.".to_string(),
963        parameters: vec![Parameter {
964            name: "client_id".to_string(),
965            type_hint: "number".to_string(),
966            description: "Client ID to reconnect (e.g., 1, 2).".to_string(),
967            required: true,
968        }],
969        example: json!({
970            "type": "reconnect_client",
971            "client_id": 1
972        }),
973        log_template: None,
974    }
975}
976
977/// Get action definition for update_client_instruction
978pub fn update_client_instruction_action() -> ActionDefinition {
979    ActionDefinition {
980        name: "update_client_instruction".to_string(),
981        description:
982            "Update the instruction for a specific client (replaces existing instruction)."
983                .to_string(),
984        parameters: vec![
985            Parameter {
986                name: "client_id".to_string(),
987                type_hint: "number".to_string(),
988                description: "Client ID to update (e.g., 1, 2).".to_string(),
989                required: true,
990            },
991            Parameter {
992                name: "instruction".to_string(),
993                type_hint: "string".to_string(),
994                description: "New instruction for the client.".to_string(),
995                required: true,
996            },
997        ],
998        example: json!({
999            "type": "update_client_instruction",
1000            "client_id": 1,
1001            "instruction": "Switch to POST requests with JSON payload"
1002        }),
1003        log_template: None,
1004    }
1005}
1006
1007/// Get action definition for update_server
1008pub fn update_server_action() -> ActionDefinition {
1009    ActionDefinition {
1010        name: "update_server".to_string(),
1011        description: "Update a RUNNING server in place, by its id, instead of opening a second \
1012            one. Supply only the fields you want to change. Changing the instruction, memory, \
1013            event_handlers, feedback_instructions or scheduled_tasks is applied WITHOUT dropping \
1014            connections. Changing the port, host, interface, mac_address or startup_params \
1015            requires a clean stop+start (connections are dropped and the server gets a new id). \
1016            Prefer this over open_server when a server for the protocol already exists."
1017            .to_string(),
1018        parameters: vec![
1019            Parameter {
1020                name: "server_id".to_string(),
1021                type_hint: "number".to_string(),
1022                description: "Id of the server to update (from the running servers list)."
1023                    .to_string(),
1024                required: true,
1025            },
1026            Parameter {
1027                name: "instruction".to_string(),
1028                type_hint: "string".to_string(),
1029                description: "New LLM instruction (hot-applied).".to_string(),
1030                required: false,
1031            },
1032            Parameter {
1033                name: "event_handlers".to_string(),
1034                type_hint: "array".to_string(),
1035                description: "Replacement deterministic event handlers (hot-applied). Same shape \
1036                    as open_server's."
1037                    .to_string(),
1038                required: false,
1039            },
1040            Parameter {
1041                name: "startup_params".to_string(),
1042                type_hint: "object".to_string(),
1043                description: "Startup parameters to merge in. Validated against the protocol \
1044                    schema; triggers a restart."
1045                    .to_string(),
1046                required: false,
1047            },
1048            Parameter {
1049                name: "port".to_string(),
1050                type_hint: "number".to_string(),
1051                description: "New port to bind. Triggers a restart.".to_string(),
1052                required: false,
1053            },
1054        ],
1055        example: json!({
1056            "type": "update_server",
1057            "server_id": 1,
1058            "instruction": "Return 503 for every request now."
1059        }),
1060        log_template: None,
1061    }
1062}
1063
1064/// Get action definition for update_client
1065pub fn update_client_action() -> ActionDefinition {
1066    ActionDefinition {
1067        name: "update_client".to_string(),
1068        description: "Update a RUNNING client in place, by its id, instead of opening a second \
1069            one. Supply only the fields you want to change. Changing the instruction, memory, \
1070            event_handlers, feedback_instructions or scheduled_tasks is applied in place. \
1071            Changing the remote_addr or startup_params reconnects (new id)."
1072            .to_string(),
1073        parameters: vec![
1074            Parameter {
1075                name: "client_id".to_string(),
1076                type_hint: "number".to_string(),
1077                description: "Id of the client to update.".to_string(),
1078                required: true,
1079            },
1080            Parameter {
1081                name: "instruction".to_string(),
1082                type_hint: "string".to_string(),
1083                description: "New LLM instruction (hot-applied).".to_string(),
1084                required: false,
1085            },
1086            Parameter {
1087                name: "event_handlers".to_string(),
1088                type_hint: "array".to_string(),
1089                description: "Replacement deterministic event handlers (hot-applied).".to_string(),
1090                required: false,
1091            },
1092            Parameter {
1093                name: "remote_addr".to_string(),
1094                type_hint: "string".to_string(),
1095                description: "New remote address 'host:port'. Triggers a reconnect.".to_string(),
1096                required: false,
1097            },
1098        ],
1099        example: json!({
1100            "type": "update_client",
1101            "client_id": 1,
1102            "instruction": "Log every response you receive."
1103        }),
1104        log_template: None,
1105    }
1106}
1107
1108/// Get action definition for update_instruction
1109pub fn update_instruction_action() -> ActionDefinition {
1110    ActionDefinition {
1111        name: "update_instruction".to_string(),
1112        description: "Update the current server instruction (combines with existing instruction)"
1113            .to_string(),
1114        parameters: vec![Parameter {
1115            name: "instruction".to_string(),
1116            type_hint: "string".to_string(),
1117            description: "New instruction to add/combine".to_string(),
1118            required: true,
1119        }],
1120        example: json!({
1121            "type": "update_instruction",
1122            "instruction": "For all HTTP requests, return status 404 with 'Not Found' message."
1123        }),
1124        log_template: None,
1125    }
1126}
1127
1128/// Get action definition for change_model
1129pub fn change_model_action() -> ActionDefinition {
1130    ActionDefinition {
1131        name: "change_model".to_string(),
1132        description: "Switch to a different LLM model".to_string(),
1133        parameters: vec![Parameter {
1134            name: "model".to_string(),
1135            type_hint: "string".to_string(),
1136            description: "Model name (e.g., 'llama3.2:latest')".to_string(),
1137            required: true,
1138        }],
1139        example: json!({
1140            "type": "change_model",
1141            "model": "llama3.2:latest"
1142        }),
1143        log_template: None,
1144    }
1145}
1146
1147/// Get action definition for set_memory
1148pub fn set_memory_action() -> ActionDefinition {
1149    ActionDefinition {
1150        name: "set_memory".to_string(),
1151        description: "Replace the entire global memory with new content. Any existing memory is discarded. Use this to reset or completely rewrite memory state.".to_string(),
1152        parameters: vec![
1153            Parameter {
1154                name: "value".to_string(),
1155                type_hint: "string".to_string(),
1156                description: "New memory value as a string. Replaces all existing memory.".to_string(),
1157                required: true,
1158            },
1159        ],
1160        example: json!({
1161            "type": "set_memory",
1162            "value": "session_id: abc123\nuser_preferences: dark_mode=true\nlast_command: LIST"
1163        }),
1164        log_template: None,
1165    }
1166}
1167
1168/// Get action definition for append_memory
1169pub fn append_memory_action() -> ActionDefinition {
1170    ActionDefinition {
1171        name: "append_memory".to_string(),
1172        description: "Add new content to the end of global memory. Existing memory is preserved and a newline is automatically added before the new content. Use this to incrementally build up memory state.".to_string(),
1173        parameters: vec![
1174            Parameter {
1175                name: "value".to_string(),
1176                type_hint: "string".to_string(),
1177                description: "Text to append as a string. Will be added after existing memory with newline separator.".to_string(),
1178                required: true,
1179            },
1180        ],
1181        example: json!({
1182            "type": "append_memory",
1183            "value": "connection_count: 5\nlast_file_requested: readme.md"
1184        }),
1185        log_template: None,
1186    }
1187}
1188
1189/// Get action definition for append_to_log
1190pub fn append_to_log_action() -> ActionDefinition {
1191    ActionDefinition {
1192        name: "append_to_log".to_string(),
1193        description: "If you are asked to log information for the user, use this to append logs to a file. Use this to create access logs, audit trails, or any persistent logging.".to_string(),
1194        parameters: vec![
1195            Parameter {
1196                name: "output_name".to_string(),
1197                type_hint: "string".to_string(),
1198                description: "Name of the log output (e.g., 'access_logs'). Used to construct the log filename netget_<name>_<time>.log, so it may contain only A-Z, a-z, 0-9, '_' and '-' (1-64 characters); anything else is refused.".to_string(),
1199                required: true,
1200            },
1201            Parameter {
1202                name: "content".to_string(),
1203                type_hint: "string".to_string(),
1204                description: "Content to append to the log file.".to_string(),
1205                required: true,
1206            },
1207        ],
1208        example: json!({
1209            "type": "append_to_log",
1210            "output_name": "access_logs",
1211            "content": "127.0.0.1 - - [29/Oct/2025:12:34:56 +0000] \"GET /index.html HTTP/1.1\" 200 1234"
1212        }),
1213        log_template: None,
1214    }
1215}
1216
1217/// Get action definition for provide_feedback
1218pub fn provide_feedback_action() -> ActionDefinition {
1219    ActionDefinition {
1220        name: "provide_feedback".to_string(),
1221        description: "Provide feedback for automatic server/client adjustment. Only available when feedback_instructions was set during server/client creation. Feedback is accumulated and debounced (leading edge), then periodically the LLM is invoked with the feedback_instructions to decide how to adjust the server/client behavior. Use this to signal issues, patterns, or learning opportunities that should trigger automatic adaptation.".to_string(),
1222        parameters: vec![
1223            Parameter {
1224                name: "feedback".to_string(),
1225                type_hint: "object".to_string(),
1226                description: "Structured feedback data describing what should be learned or adjusted. Can include any relevant context like error rates, performance metrics, client behaviors, failed requests, etc. Example: {\"issue\": \"client_timeout\", \"details\": \"Client disconnected after 5s\", \"suggestion\": \"Increase response speed\"}".to_string(),
1227                required: true,
1228            },
1229        ],
1230        example: json!({
1231            "type": "provide_feedback",
1232            "feedback": {
1233                "issue": "authentication_failed",
1234                "username": "guest",
1235                "attempts": 3,
1236                "suggestion": "Consider blocking IP after multiple failures"
1237            }
1238        }),
1239        log_template: None,
1240    }
1241}
1242
1243/// Get action definition for schedule_task
1244pub fn schedule_task_action(
1245    selected_mode: crate::state::app_state::ScriptingMode,
1246    env: &crate::scripting::ScriptingEnvironment,
1247) -> ActionDefinition {
1248    let mut parameters = vec![
1249        Parameter {
1250            name: "task_id".to_string(),
1251            type_hint: "string".to_string(),
1252            description: "Unique identifier for this task (e.g., 'cleanup_logs', 'sse_heartbeat'). Used to reference or cancel the task later.".to_string(),
1253            required: true,
1254        },
1255        Parameter {
1256            name: "recurring".to_string(),
1257            type_hint: "boolean".to_string(),
1258            description: "True for recurring task (executes at intervals), false for one-shot task (executes once after delay).".to_string(),
1259            required: true,
1260        },
1261        Parameter {
1262            name: "delay_secs".to_string(),
1263            type_hint: "number".to_string(),
1264            description: "For one-shot tasks (recurring=false): delay in seconds before executing. For recurring tasks: optional initial delay before first execution (defaults to interval_secs if not provided).".to_string(),
1265            required: false,
1266        },
1267        Parameter {
1268            name: "interval_secs".to_string(),
1269            type_hint: "number".to_string(),
1270            description: "For recurring tasks (recurring=true): interval in seconds between executions. Required when recurring=true.".to_string(),
1271            required: false,
1272        },
1273        Parameter {
1274            name: "max_executions".to_string(),
1275            type_hint: "number".to_string(),
1276            description: "For recurring tasks: maximum number of times to execute. If omitted, task runs indefinitely until cancelled.".to_string(),
1277            required: false,
1278        },
1279        Parameter {
1280            name: "server_id".to_string(),
1281            type_hint: "number".to_string(),
1282            description: "Optional: Server ID to scope this task to. If provided, task uses server's instruction and protocol actions. If omitted, task is global and uses user input actions.".to_string(),
1283            required: false,
1284        },
1285        Parameter {
1286            name: "connection_id".to_string(),
1287            type_hint: "string".to_string(),
1288            description: "Optional: Connection ID (e.g., 'conn-123') to scope this task to a specific connection. Requires server_id to be specified. Task will be automatically cleaned up when the connection closes. Useful for connection-specific timeouts, session cleanup, or per-connection monitoring.".to_string(),
1289            required: false,
1290        },
1291        Parameter {
1292            name: "client_id".to_string(),
1293            type_hint: "number".to_string(),
1294            description: "Optional: Client ID to scope this task to. If provided, task uses client's instruction and protocol actions. Task will be automatically cleaned up when the client disconnects. Useful for client-specific timeouts, reconnection logic, or per-client monitoring.".to_string(),
1295            required: false,
1296        },
1297        Parameter {
1298            name: "instruction".to_string(),
1299            type_hint: "string".to_string(),
1300            description: "Instruction/prompt for LLM when task executes. Describes what the task should do.".to_string(),
1301            required: true,
1302        },
1303        Parameter {
1304            name: "context".to_string(),
1305            type_hint: "object".to_string(),
1306            description: "Optional: Additional context data to pass to LLM when task executes (e.g., thresholds, parameters).".to_string(),
1307            required: false,
1308        },
1309    ];
1310
1311    // Add script parameters based on scripting mode
1312    match selected_mode {
1313        crate::state::app_state::ScriptingMode::On => {
1314            // ON mode: LLM chooses runtime from available options
1315            let available_runtimes = env.format_available();
1316            parameters.extend(vec![
1317                Parameter {
1318                    name: "script_runtime".to_string(),
1319                    type_hint: "string".to_string(),
1320                    description: format!(
1321                        "Required when script_inline is provided: Choose runtime for script execution. Available: {}",
1322                        available_runtimes
1323                    ),
1324                    required: false,
1325                },
1326                Parameter {
1327                    name: "script_inline".to_string(),
1328                    type_hint: "string".to_string(),
1329                    description: "Optional: Inline script code to handle task execution instead of LLM. Must match the script_runtime language. If provided, script_runtime MUST also be specified.".to_string(),
1330                    required: false,
1331                },
1332                Parameter {
1333                    name: "script_handles".to_string(),
1334                    type_hint: "array".to_string(),
1335                    description: "Optional: Event types the script handles (e.g., [\"scheduled_task_cleanup\"]). Defaults to [\"all\"].".to_string(),
1336                    required: false,
1337                },
1338            ]);
1339        }
1340        crate::state::app_state::ScriptingMode::Off => {
1341            // OFF mode: no script parameters
1342        }
1343        crate::state::app_state::ScriptingMode::Python
1344        | crate::state::app_state::ScriptingMode::JavaScript
1345        | crate::state::app_state::ScriptingMode::Go
1346        | crate::state::app_state::ScriptingMode::Perl => {
1347            let lang = selected_mode.as_str();
1348            parameters.extend(vec![
1349                Parameter {
1350                    name: "script_inline".to_string(),
1351                    type_hint: "string".to_string(),
1352                    description: format!(
1353                        "Optional: Inline {} script code to handle task execution instead of LLM. If provided, the script will be executed for each task trigger.",
1354                        lang
1355                    ),
1356                    required: false,
1357                },
1358                Parameter {
1359                    name: "script_handles".to_string(),
1360                    type_hint: "array".to_string(),
1361                    description: "Optional: Event types the script handles (e.g., [\"scheduled_task_cleanup\"]). Defaults to [\"all\"].".to_string(),
1362                    required: false,
1363                },
1364            ]);
1365        }
1366    }
1367
1368    ActionDefinition {
1369        name: "schedule_task".to_string(),
1370        description: "Schedule a task (one-shot or recurring). The task will call the LLM or execute a script with the provided instruction. One-shot tasks execute once after a delay and are automatically removed. Recurring tasks execute at intervals until cancelled or max_executions is reached. Useful for delayed operations, timeouts, periodic health checks, heartbeats, SSE messages, metrics collection, etc.".to_string(),
1371        parameters,
1372        example: json!({
1373            "type": "schedule_task",
1374            "task_id": "sse_heartbeat",
1375            "recurring": true,
1376            "interval_secs": 30,
1377            "server_id": 1,
1378            "instruction": "Send SSE heartbeat to all active connections"
1379        }),
1380        log_template: None,
1381    }
1382}
1383
1384/// Get action definition for cancel_task
1385pub fn cancel_task_action() -> ActionDefinition {
1386    ActionDefinition {
1387        name: "cancel_task".to_string(),
1388        description: "Cancel a scheduled task by its task_id. Works for both one-shot and recurring tasks. The task is immediately removed and will not execute again.".to_string(),
1389        parameters: vec![Parameter {
1390            name: "task_id".to_string(),
1391            type_hint: "string".to_string(),
1392            description: "ID of the task to cancel (the task_id used when scheduling).".to_string(),
1393            required: true,
1394        }],
1395        example: json!({
1396            "type": "cancel_task",
1397            "task_id": "cleanup_logs"
1398        }),
1399        log_template: None,
1400    }
1401}
1402
1403/// Get action definition for create_database
1404#[cfg(feature = "sqlite")]
1405pub fn create_database_action() -> ActionDefinition {
1406    ActionDefinition {
1407        name: "create_database".to_string(),
1408        description: "Create a new SQLite database (in-memory or file-based). Use this to store protocol state (e.g., NFS file system, DNS cache, user sessions). The database persists for the lifetime of the owning server/client, or forever if global. You can execute DDL to create tables during creation.".to_string(),
1409        parameters: vec![
1410            Parameter {
1411                name: "name".to_string(),
1412                type_hint: "string".to_string(),
1413                description: "Database name (user-friendly identifier). This will be used to construct the filename as './netget_db_<name>.db' for file-based databases.".to_string(),
1414                required: true,
1415            },
1416            Parameter {
1417                name: "is_memory".to_string(),
1418                type_hint: "boolean".to_string(),
1419                description: "true = in-memory database (fast, data lost on close), false = file-based database (persistent, saved to ./netget_db_<name>.db). Defaults to false (file-based).".to_string(),
1420                required: false,
1421            },
1422            Parameter {
1423                name: "owner".to_string(),
1424                type_hint: "string".to_string(),
1425                description: "Owner scope: 'server-N' (auto-deleted when server closes), 'client-N' (auto-deleted when client disconnects), or 'global' (persists across servers/clients). Omit to default to current context.".to_string(),
1426                required: false,
1427            },
1428            Parameter {
1429                name: "schema_ddl".to_string(),
1430                type_hint: "string".to_string(),
1431                description: "SQL DDL statements to create initial schema (e.g., 'CREATE TABLE files (path TEXT PRIMARY KEY, content BLOB);'). Use semicolons to separate multiple statements.".to_string(),
1432                required: false,
1433            },
1434        ],
1435        example: json!({
1436            "type": "create_database",
1437            "name": "nfs_storage",
1438            "is_memory": true,
1439            "owner": "server-1",
1440            "schema_ddl": "CREATE TABLE files (path TEXT PRIMARY KEY, content BLOB, size INTEGER, modified INTEGER);"
1441        }),
1442        log_template: None,
1443    }
1444}
1445
1446/// Get action definition for delete_database
1447#[cfg(feature = "sqlite")]
1448pub fn delete_database_action() -> ActionDefinition {
1449    ActionDefinition {
1450        name: "delete_database".to_string(),
1451        description: "Delete a database and remove its file (if file-based). This is permanent and cannot be undone. Server/client-owned databases are automatically deleted when the owner closes.".to_string(),
1452        parameters: vec![
1453            Parameter {
1454                name: "database_id".to_string(),
1455                type_hint: "number".to_string(),
1456                description: "Database ID to delete".to_string(),
1457                required: true,
1458            },
1459        ],
1460        example: json!({
1461            "type": "delete_database",
1462            "database_id": 1
1463        }),
1464        log_template: None,
1465    }
1466}
1467
1468/// Get all common action definitions
1469///
1470/// Actions are organized logically:
1471/// 1. Server Management - Create/destroy servers
1472/// 2. Client Management - Create/destroy/control clients
1473/// 3. Server Configuration - Configure running servers
1474/// 4. Task Management - Schedule/cancel tasks
1475/// 5. Database Management - Create/query/manage SQLite databases
1476/// 6. System/Utility - Model changes, messages, logging
1477pub fn get_all_common_actions(
1478    selected_mode: crate::state::app_state::ScriptingMode,
1479    env: &crate::scripting::ScriptingEnvironment,
1480    is_open_server_enabled: bool,
1481    is_open_client_enabled: bool,
1482) -> Vec<ActionDefinition> {
1483    #[allow(unused_mut)]
1484    let mut actions = vec![];
1485
1486    // === Server Management ===
1487    // Only include open_server when enabled (after docs are read)
1488    if is_open_server_enabled {
1489        actions.push(open_server_action(selected_mode, env, true));
1490    }
1491    actions.push(close_server_action());
1492    actions.push(close_all_servers_action());
1493
1494    // === Client Management ===
1495    // Only include open_client when enabled (after docs are read)
1496    if is_open_client_enabled {
1497        actions.push(open_client_action(selected_mode, env, true));
1498    }
1499    actions.push(close_client_action());
1500    actions.push(close_all_clients_action());
1501
1502    // === Connection Management ===
1503    actions.push(close_connection_by_id_action());
1504
1505    // === Client Configuration ===
1506    actions.push(reconnect_client_action());
1507    actions.push(update_client_instruction_action());
1508    actions.push(update_client_action());
1509
1510    // === Server Configuration ===
1511    actions.push(update_server_action());
1512    actions.push(update_instruction_action());
1513    actions.push(set_memory_action());
1514    actions.push(append_memory_action());
1515
1516    // === Task Management ===
1517    actions.push(schedule_task_action(selected_mode, env));
1518    actions.push(cancel_task_action());
1519
1520    // === System/Utility ===
1521    // actions.push(change_model_action());
1522    actions.push(show_message_action());
1523    actions.push(append_to_log_action());
1524
1525    // === Database Management ===
1526    #[cfg(feature = "sqlite")]
1527    {
1528        actions.push(create_database_action());
1529        actions.push(delete_database_action());
1530    }
1531
1532    actions
1533}
1534
1535/// Get common actions for user input (all common actions with enhanced open_server and open_client)
1536pub fn get_user_input_common_actions(
1537    selected_mode: crate::state::app_state::ScriptingMode,
1538    env: &crate::scripting::ScriptingEnvironment,
1539    is_open_server_enabled: bool,
1540    is_open_client_enabled: bool,
1541) -> Vec<ActionDefinition> {
1542    get_all_common_actions(
1543        selected_mode,
1544        env,
1545        is_open_server_enabled,
1546        is_open_client_enabled,
1547    )
1548}
1549
1550/// Get common actions for network events (exclude server management actions)
1551///
1552/// Network events only get server configuration and utility actions,
1553/// not server management (can't create/destroy servers from event handlers).
1554pub fn get_network_event_common_actions() -> Vec<ActionDefinition> {
1555    vec![
1556        // === Server Configuration ===
1557        set_memory_action(),
1558        append_memory_action(),
1559        // === System/Utility ===
1560        show_message_action(),
1561        append_to_log_action(),
1562        // === Feedback/Learning ===
1563        // Note: provide_feedback is added conditionally in action_helper.rs
1564        // only when feedback_instructions is set on the server/client
1565    ]
1566}
1567
1568/// Get all protocol names that should be available to the LLM
1569/// Filters out protocols with ProtocolState::Disabled unless include_disabled is true
1570/// Dynamically determined from the ProtocolRegistry based on compiled features
1571fn all_base_stacks(
1572    include_disabled: bool,
1573    min_stability: Option<crate::protocol::metadata::DevelopmentState>,
1574) -> Vec<String> {
1575    let registry = crate::protocol::server_registry::registry();
1576
1577    // Get all registered protocols from the registry (only includes compiled features)
1578    let mut protocols: Vec<String> = registry
1579        .all_protocols()
1580        .into_iter()
1581        .map(|(protocol_name, _protocol)| protocol_name)
1582        .filter(|protocol_name| {
1583            let meta = registry.metadata(protocol_name);
1584            // Availability to the model: Incomplete is hidden unless
1585            // --include-disabled-protocols overrides it.
1586            let available = include_disabled
1587                || meta
1588                    .as_ref()
1589                    .map(|m| m.is_available_to_llm())
1590                    .unwrap_or(true);
1591            // Operator floor: never offer a protocol below --min-stability, even
1592            // when include_disabled is set. Unknown metadata is not hidden.
1593            let meets_min = match (min_stability, meta.as_ref()) {
1594                (Some(min), Some(m)) => m.state >= min,
1595                _ => true,
1596            };
1597            available && meets_min
1598        })
1599        .collect();
1600
1601    // Sort protocols alphabetically for deterministic output
1602    protocols.sort();
1603    protocols
1604}
1605
1606/// Generate comprehensive base stack documentation with startup parameters
1607/// Returns formatted text listing all available stacks and their configuration options
1608pub fn generate_base_stack_documentation(
1609    include_disabled: bool,
1610    min_stability: Option<crate::protocol::metadata::DevelopmentState>,
1611) -> String {
1612    let mut doc = String::from("## Available Base Stacks\n\n");
1613
1614    // Group protocols by their group_name
1615    let registry = crate::protocol::server_registry::registry();
1616    let mut groups: ProtocolGroups = std::collections::HashMap::new();
1617
1618    for protocol_name in all_base_stacks(include_disabled, min_stability) {
1619        if let Some(protocol) = registry.get(&protocol_name) {
1620            let group = protocol.group_name();
1621            groups
1622                .entry(group)
1623                .or_default()
1624                .push((protocol_name.clone(), protocol));
1625        }
1626    }
1627
1628    // Sort groups alphabetically
1629    let mut sorted_group_names: Vec<&'static str> = groups.keys().copied().collect();
1630    sorted_group_names.sort();
1631
1632    for group_name in sorted_group_names {
1633        if let Some(protocols) = groups.get_mut(group_name) {
1634            if protocols.is_empty() {
1635                continue;
1636            }
1637
1638            // Output group header
1639            doc.push_str(&format!("### {}\n", group_name));
1640
1641            // Sort protocols alphabetically within group
1642            protocols.sort_by(|a, b| a.0.cmp(&b.0));
1643
1644            // Output each protocol with keywords on one line
1645            for (protocol_name, protocol) in protocols {
1646                let keywords = protocol.keywords();
1647                if !keywords.is_empty() {
1648                    doc.push_str(&format!("{} ({})\n", protocol_name, keywords.join(", ")));
1649                } else {
1650                    doc.push_str(&format!("{}\n", protocol_name));
1651                }
1652            }
1653            doc.push('\n');
1654        }
1655    }
1656
1657    doc
1658}
1659
1660/// Structured protocol documentation data for templates
1661#[derive(Debug, serde::Serialize)]
1662pub struct ProtocolDocData {
1663    pub protocol_name: String,
1664    pub both_modes: bool,
1665    pub server: Option<ProtocolModeData>,
1666    pub client: Option<ProtocolModeData>,
1667}
1668
1669/// Documentation for a single mode (server or client)
1670#[derive(Debug, serde::Serialize)]
1671pub struct ProtocolModeData {
1672    pub stack_name: String,
1673    pub group_name: String,
1674    pub description: String,
1675    pub example_prompt: String,
1676    pub keywords: Vec<String>,
1677    pub startup_params: Vec<super::ParameterDefinition>,
1678    pub state: String,
1679    pub notes: Option<String>,
1680}
1681
1682/// Generate structured documentation data for a single protocol
1683///
1684/// This is used by the read_base_stack_docs tool to provide detailed information
1685/// about a specific protocol on demand. Includes both server and client capabilities
1686/// if available.
1687///
1688/// # Arguments
1689/// * `protocol_name` - Name of the protocol (e.g., "http", "ssh", "tor")
1690///
1691/// # Returns
1692/// * `Ok(ProtocolDocData)` - Structured documentation for the protocol (server and/or client)
1693/// * `Err(_)` - If protocol not found in either registry
1694pub fn generate_single_protocol_doc_data(protocol_name: &str) -> anyhow::Result<ProtocolDocData> {
1695    let server_registry = crate::protocol::server_registry::registry();
1696    let client_registry = &crate::protocol::client_registry::CLIENT_REGISTRY;
1697
1698    // Protocol names are stored in uppercase (e.g., "HTTP", "SSH", "TCP")
1699    // Normalize input to lowercase for client registry and uppercase for server
1700    let normalized_name_upper = protocol_name.to_uppercase();
1701    let normalized_name_lower = protocol_name.to_lowercase();
1702
1703    let server_protocol = server_registry.get(&normalized_name_upper);
1704    let client_protocol = client_registry.get(&normalized_name_lower);
1705
1706    // Error if neither found
1707    if server_protocol.is_none() && client_protocol.is_none() {
1708        return Err(anyhow::anyhow!(
1709            "Protocol '{}' not found in server or client registry",
1710            protocol_name
1711        ));
1712    }
1713
1714    let both_modes = server_protocol.is_some() && client_protocol.is_some();
1715
1716    // Build server mode data
1717    let server = server_protocol.map(|protocol| {
1718        let metadata = protocol.metadata();
1719        // Format startup parameters with JSON-serialized examples
1720        let mut startup_params = protocol.get_startup_parameters();
1721        for param in &mut startup_params {
1722            // Convert the example Value to a formatted JSON string
1723            if let Ok(json_str) = serde_json::to_string(&param.example) {
1724                param.example = serde_json::Value::String(json_str);
1725            }
1726        }
1727        ProtocolModeData {
1728            stack_name: protocol.stack_name().to_string(),
1729            group_name: protocol.group_name().to_string(),
1730            description: protocol.description().to_string(),
1731            example_prompt: protocol.example_prompt().to_string(),
1732            keywords: protocol.keywords().iter().map(|s| s.to_string()).collect(),
1733            startup_params,
1734            state: format!("{:?}", metadata.state),
1735            notes: metadata.notes.map(|s| s.to_string()),
1736        }
1737    });
1738
1739    // Build client mode data
1740    let client = client_protocol.map(|protocol| {
1741        let metadata = protocol.metadata();
1742        // Format startup parameters with JSON-serialized examples
1743        let mut startup_params = protocol.get_startup_parameters();
1744        for param in &mut startup_params {
1745            // Convert the example Value to a formatted JSON string
1746            if let Ok(json_str) = serde_json::to_string(&param.example) {
1747                param.example = serde_json::Value::String(json_str);
1748            }
1749        }
1750        ProtocolModeData {
1751            stack_name: protocol.stack_name().to_string(),
1752            group_name: protocol.group_name().to_string(),
1753            description: protocol.description().to_string(),
1754            example_prompt: protocol.example_prompt().to_string(),
1755            keywords: protocol.keywords().iter().map(|s| s.to_string()).collect(),
1756            startup_params,
1757            state: format!("{:?}", metadata.state),
1758            notes: metadata.notes.map(|s| s.to_string()),
1759        }
1760    });
1761
1762    Ok(ProtocolDocData {
1763        protocol_name: protocol_name.to_uppercase(),
1764        both_modes,
1765        server,
1766        client,
1767    })
1768}