Skip to main content

harn_vm/
stdlib.rs

1use std::collections::BTreeMap;
2use std::io::BufRead;
3use std::rc::Rc;
4use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU8, Ordering};
5use std::sync::Arc;
6
7use crate::value::{values_equal, VmAtomicHandle, VmChannelHandle, VmError, VmValue};
8use crate::vm::Vm;
9
10use crate::http::register_http_builtins;
11use crate::llm::register_llm_builtins;
12use crate::mcp::register_mcp_builtins;
13
14/// Register standard builtins on a VM.
15pub fn register_vm_stdlib(vm: &mut Vm) {
16    vm.register_builtin("log", |args, out| {
17        let msg = args.first().map(|a| a.display()).unwrap_or_default();
18        out.push_str(&format!("[harn] {msg}\n"));
19        Ok(VmValue::Nil)
20    });
21    vm.register_builtin("print", |args, out| {
22        let msg = args.first().map(|a| a.display()).unwrap_or_default();
23        out.push_str(&msg);
24        Ok(VmValue::Nil)
25    });
26    vm.register_builtin("println", |args, out| {
27        let msg = args.first().map(|a| a.display()).unwrap_or_default();
28        out.push_str(&format!("{msg}\n"));
29        Ok(VmValue::Nil)
30    });
31    vm.register_builtin("type_of", |args, _out| {
32        let val = args.first().unwrap_or(&VmValue::Nil);
33        Ok(VmValue::String(Rc::from(val.type_name())))
34    });
35    vm.register_builtin("to_string", |args, _out| {
36        let val = args.first().unwrap_or(&VmValue::Nil);
37        Ok(VmValue::String(Rc::from(val.display())))
38    });
39    vm.register_builtin("to_int", |args, _out| {
40        let val = args.first().unwrap_or(&VmValue::Nil);
41        match val {
42            VmValue::Int(n) => Ok(VmValue::Int(*n)),
43            VmValue::Float(n) => Ok(VmValue::Int(*n as i64)),
44            VmValue::String(s) => Ok(s.parse::<i64>().map(VmValue::Int).unwrap_or(VmValue::Nil)),
45            _ => Ok(VmValue::Nil),
46        }
47    });
48    vm.register_builtin("to_float", |args, _out| {
49        let val = args.first().unwrap_or(&VmValue::Nil);
50        match val {
51            VmValue::Float(n) => Ok(VmValue::Float(*n)),
52            VmValue::Int(n) => Ok(VmValue::Float(*n as f64)),
53            VmValue::String(s) => Ok(s.parse::<f64>().map(VmValue::Float).unwrap_or(VmValue::Nil)),
54            _ => Ok(VmValue::Nil),
55        }
56    });
57
58    vm.register_builtin("json_stringify", |args, _out| {
59        let val = args.first().unwrap_or(&VmValue::Nil);
60        Ok(VmValue::String(Rc::from(vm_value_to_json(val))))
61    });
62
63    vm.register_builtin("json_parse", |args, _out| {
64        let text = args.first().map(|a| a.display()).unwrap_or_default();
65        match serde_json::from_str::<serde_json::Value>(&text) {
66            Ok(jv) => Ok(json_to_vm_value(&jv)),
67            Err(e) => Err(VmError::Thrown(VmValue::String(Rc::from(format!(
68                "JSON parse error: {e}"
69            ))))),
70        }
71    });
72
73    vm.register_builtin("env", |args, _out| {
74        let name = args.first().map(|a| a.display()).unwrap_or_default();
75        match std::env::var(&name) {
76            Ok(val) => Ok(VmValue::String(Rc::from(val))),
77            Err(_) => Ok(VmValue::Nil),
78        }
79    });
80
81    vm.register_builtin("timestamp", |_args, _out| {
82        use std::time::{SystemTime, UNIX_EPOCH};
83        let secs = SystemTime::now()
84            .duration_since(UNIX_EPOCH)
85            .map(|d| d.as_secs_f64())
86            .unwrap_or(0.0);
87        Ok(VmValue::Float(secs))
88    });
89
90    vm.register_builtin("read_file", |args, _out| {
91        let path = args.first().map(|a| a.display()).unwrap_or_default();
92        match std::fs::read_to_string(&path) {
93            Ok(content) => Ok(VmValue::String(Rc::from(content))),
94            Err(e) => Err(VmError::Thrown(VmValue::String(Rc::from(format!(
95                "Failed to read file {path}: {e}"
96            ))))),
97        }
98    });
99
100    vm.register_builtin("write_file", |args, _out| {
101        if args.len() >= 2 {
102            let path = args[0].display();
103            let content = args[1].display();
104            std::fs::write(&path, &content).map_err(|e| {
105                VmError::Thrown(VmValue::String(Rc::from(format!(
106                    "Failed to write file {path}: {e}"
107                ))))
108            })?;
109        }
110        Ok(VmValue::Nil)
111    });
112
113    vm.register_builtin("exit", |args, _out| {
114        let code = args.first().and_then(|a| a.as_int()).unwrap_or(0);
115        std::process::exit(code as i32);
116    });
117
118    vm.register_builtin("regex_match", |args, _out| {
119        if args.len() >= 2 {
120            let pattern = args[0].display();
121            let text = args[1].display();
122            let re = regex::Regex::new(&pattern).map_err(|e| {
123                VmError::Thrown(VmValue::String(Rc::from(format!("Invalid regex: {e}"))))
124            })?;
125            let matches: Vec<VmValue> = re
126                .find_iter(&text)
127                .map(|m| VmValue::String(Rc::from(m.as_str())))
128                .collect();
129            if matches.is_empty() {
130                return Ok(VmValue::Nil);
131            }
132            return Ok(VmValue::List(Rc::new(matches)));
133        }
134        Ok(VmValue::Nil)
135    });
136
137    vm.register_builtin("regex_replace", |args, _out| {
138        if args.len() >= 3 {
139            let pattern = args[0].display();
140            let replacement = args[1].display();
141            let text = args[2].display();
142            let re = regex::Regex::new(&pattern).map_err(|e| {
143                VmError::Thrown(VmValue::String(Rc::from(format!("Invalid regex: {e}"))))
144            })?;
145            return Ok(VmValue::String(Rc::from(
146                re.replace_all(&text, replacement.as_str()).into_owned(),
147            )));
148        }
149        Ok(VmValue::Nil)
150    });
151
152    vm.register_builtin("prompt_user", |args, out| {
153        let msg = args.first().map(|a| a.display()).unwrap_or_default();
154        out.push_str(&msg);
155        let mut input = String::new();
156        if std::io::stdin().lock().read_line(&mut input).is_ok() {
157            Ok(VmValue::String(Rc::from(input.trim_end())))
158        } else {
159            Ok(VmValue::Nil)
160        }
161    });
162
163    // --- Math builtins ---
164
165    vm.register_builtin("abs", |args, _out| {
166        match args.first().unwrap_or(&VmValue::Nil) {
167            VmValue::Int(n) => Ok(VmValue::Int(n.wrapping_abs())),
168            VmValue::Float(n) => Ok(VmValue::Float(n.abs())),
169            _ => Ok(VmValue::Nil),
170        }
171    });
172
173    vm.register_builtin("min", |args, _out| {
174        if args.len() >= 2 {
175            match (&args[0], &args[1]) {
176                (VmValue::Int(x), VmValue::Int(y)) => Ok(VmValue::Int(*x.min(y))),
177                (VmValue::Float(x), VmValue::Float(y)) => Ok(VmValue::Float(x.min(*y))),
178                (VmValue::Int(x), VmValue::Float(y)) => Ok(VmValue::Float((*x as f64).min(*y))),
179                (VmValue::Float(x), VmValue::Int(y)) => Ok(VmValue::Float(x.min(*y as f64))),
180                _ => Ok(VmValue::Nil),
181            }
182        } else {
183            Ok(VmValue::Nil)
184        }
185    });
186
187    vm.register_builtin("max", |args, _out| {
188        if args.len() >= 2 {
189            match (&args[0], &args[1]) {
190                (VmValue::Int(x), VmValue::Int(y)) => Ok(VmValue::Int(*x.max(y))),
191                (VmValue::Float(x), VmValue::Float(y)) => Ok(VmValue::Float(x.max(*y))),
192                (VmValue::Int(x), VmValue::Float(y)) => Ok(VmValue::Float((*x as f64).max(*y))),
193                (VmValue::Float(x), VmValue::Int(y)) => Ok(VmValue::Float(x.max(*y as f64))),
194                _ => Ok(VmValue::Nil),
195            }
196        } else {
197            Ok(VmValue::Nil)
198        }
199    });
200
201    vm.register_builtin("floor", |args, _out| {
202        match args.first().unwrap_or(&VmValue::Nil) {
203            VmValue::Float(n) => Ok(VmValue::Int(n.floor() as i64)),
204            VmValue::Int(n) => Ok(VmValue::Int(*n)),
205            _ => Ok(VmValue::Nil),
206        }
207    });
208
209    vm.register_builtin("ceil", |args, _out| {
210        match args.first().unwrap_or(&VmValue::Nil) {
211            VmValue::Float(n) => Ok(VmValue::Int(n.ceil() as i64)),
212            VmValue::Int(n) => Ok(VmValue::Int(*n)),
213            _ => Ok(VmValue::Nil),
214        }
215    });
216
217    vm.register_builtin("round", |args, _out| {
218        match args.first().unwrap_or(&VmValue::Nil) {
219            VmValue::Float(n) => Ok(VmValue::Int(n.round() as i64)),
220            VmValue::Int(n) => Ok(VmValue::Int(*n)),
221            _ => Ok(VmValue::Nil),
222        }
223    });
224
225    vm.register_builtin("sqrt", |args, _out| {
226        match args.first().unwrap_or(&VmValue::Nil) {
227            VmValue::Float(n) => Ok(VmValue::Float(n.sqrt())),
228            VmValue::Int(n) => Ok(VmValue::Float((*n as f64).sqrt())),
229            _ => Ok(VmValue::Nil),
230        }
231    });
232
233    vm.register_builtin("pow", |args, _out| {
234        if args.len() >= 2 {
235            match (&args[0], &args[1]) {
236                (VmValue::Int(base), VmValue::Int(exp)) => {
237                    if *exp >= 0 && *exp <= u32::MAX as i64 {
238                        Ok(VmValue::Int(base.wrapping_pow(*exp as u32)))
239                    } else {
240                        Ok(VmValue::Float((*base as f64).powf(*exp as f64)))
241                    }
242                }
243                (VmValue::Float(base), VmValue::Int(exp)) => {
244                    if *exp >= i32::MIN as i64 && *exp <= i32::MAX as i64 {
245                        Ok(VmValue::Float(base.powi(*exp as i32)))
246                    } else {
247                        Ok(VmValue::Float(base.powf(*exp as f64)))
248                    }
249                }
250                (VmValue::Int(base), VmValue::Float(exp)) => {
251                    Ok(VmValue::Float((*base as f64).powf(*exp)))
252                }
253                (VmValue::Float(base), VmValue::Float(exp)) => Ok(VmValue::Float(base.powf(*exp))),
254                _ => Ok(VmValue::Nil),
255            }
256        } else {
257            Ok(VmValue::Nil)
258        }
259    });
260
261    vm.register_builtin("random", |_args, _out| {
262        use rand::Rng;
263        let val: f64 = rand::thread_rng().gen();
264        Ok(VmValue::Float(val))
265    });
266
267    vm.register_builtin("random_int", |args, _out| {
268        use rand::Rng;
269        if args.len() >= 2 {
270            let min = args[0].as_int().unwrap_or(0);
271            let max = args[1].as_int().unwrap_or(0);
272            if min <= max {
273                let val = rand::thread_rng().gen_range(min..=max);
274                return Ok(VmValue::Int(val));
275            }
276        }
277        Ok(VmValue::Nil)
278    });
279
280    // --- Assert builtins ---
281
282    vm.register_builtin("assert", |args, _out| {
283        let condition = args.first().unwrap_or(&VmValue::Nil);
284        if !condition.is_truthy() {
285            let msg = args
286                .get(1)
287                .map(|a| a.display())
288                .unwrap_or_else(|| "Assertion failed".to_string());
289            return Err(VmError::Thrown(VmValue::String(Rc::from(msg))));
290        }
291        Ok(VmValue::Nil)
292    });
293
294    vm.register_builtin("assert_eq", |args, _out| {
295        if args.len() >= 2 {
296            if !values_equal(&args[0], &args[1]) {
297                let msg = args.get(2).map(|a| a.display()).unwrap_or_else(|| {
298                    format!(
299                        "Assertion failed: expected {}, got {}",
300                        args[1].display(),
301                        args[0].display()
302                    )
303                });
304                return Err(VmError::Thrown(VmValue::String(Rc::from(msg))));
305            }
306            Ok(VmValue::Nil)
307        } else {
308            Err(VmError::Thrown(VmValue::String(Rc::from(
309                "assert_eq requires at least 2 arguments",
310            ))))
311        }
312    });
313
314    vm.register_builtin("assert_ne", |args, _out| {
315        if args.len() >= 2 {
316            if values_equal(&args[0], &args[1]) {
317                let msg = args.get(2).map(|a| a.display()).unwrap_or_else(|| {
318                    format!(
319                        "Assertion failed: values should not be equal: {}",
320                        args[0].display()
321                    )
322                });
323                return Err(VmError::Thrown(VmValue::String(Rc::from(msg))));
324            }
325            Ok(VmValue::Nil)
326        } else {
327            Err(VmError::Thrown(VmValue::String(Rc::from(
328                "assert_ne requires at least 2 arguments",
329            ))))
330        }
331    });
332
333    vm.register_builtin("__range__", |args, _out| {
334        let start = args.first().and_then(|a| a.as_int()).unwrap_or(0);
335        let end = args.get(1).and_then(|a| a.as_int()).unwrap_or(0);
336        let inclusive = args.get(2).map(|a| a.is_truthy()).unwrap_or(false);
337        let items: Vec<VmValue> = if inclusive {
338            (start..=end).map(VmValue::Int).collect()
339        } else {
340            (start..end).map(VmValue::Int).collect()
341        };
342        Ok(VmValue::List(Rc::new(items)))
343    });
344
345    // =========================================================================
346    // File system builtins
347    // =========================================================================
348
349    vm.register_builtin("file_exists", |args, _out| {
350        let path = args.first().map(|a| a.display()).unwrap_or_default();
351        Ok(VmValue::Bool(std::path::Path::new(&path).exists()))
352    });
353
354    vm.register_builtin("delete_file", |args, _out| {
355        let path = args.first().map(|a| a.display()).unwrap_or_default();
356        let p = std::path::Path::new(&path);
357        if p.is_dir() {
358            std::fs::remove_dir_all(&path).map_err(|e| {
359                VmError::Thrown(VmValue::String(Rc::from(format!(
360                    "Failed to delete directory {path}: {e}"
361                ))))
362            })?;
363        } else {
364            std::fs::remove_file(&path).map_err(|e| {
365                VmError::Thrown(VmValue::String(Rc::from(format!(
366                    "Failed to delete file {path}: {e}"
367                ))))
368            })?;
369        }
370        Ok(VmValue::Nil)
371    });
372
373    vm.register_builtin("append_file", |args, _out| {
374        use std::io::Write;
375        if args.len() >= 2 {
376            let path = args[0].display();
377            let content = args[1].display();
378            let mut file = std::fs::OpenOptions::new()
379                .append(true)
380                .create(true)
381                .open(&path)
382                .map_err(|e| {
383                    VmError::Thrown(VmValue::String(Rc::from(format!(
384                        "Failed to open file {path}: {e}"
385                    ))))
386                })?;
387            file.write_all(content.as_bytes()).map_err(|e| {
388                VmError::Thrown(VmValue::String(Rc::from(format!(
389                    "Failed to append to file {path}: {e}"
390                ))))
391            })?;
392        }
393        Ok(VmValue::Nil)
394    });
395
396    vm.register_builtin("list_dir", |args, _out| {
397        let path = args
398            .first()
399            .map(|a| a.display())
400            .unwrap_or_else(|| ".".to_string());
401        let entries = std::fs::read_dir(&path).map_err(|e| {
402            VmError::Thrown(VmValue::String(Rc::from(format!(
403                "Failed to list directory {path}: {e}"
404            ))))
405        })?;
406        let mut result = Vec::new();
407        for entry in entries {
408            let entry =
409                entry.map_err(|e| VmError::Thrown(VmValue::String(Rc::from(e.to_string()))))?;
410            let name = entry.file_name().to_string_lossy().to_string();
411            result.push(VmValue::String(Rc::from(name.as_str())));
412        }
413        result.sort_by_key(|a| a.display());
414        Ok(VmValue::List(Rc::new(result)))
415    });
416
417    vm.register_builtin("mkdir", |args, _out| {
418        let path = args.first().map(|a| a.display()).unwrap_or_default();
419        std::fs::create_dir_all(&path).map_err(|e| {
420            VmError::Thrown(VmValue::String(Rc::from(format!(
421                "Failed to create directory {path}: {e}"
422            ))))
423        })?;
424        Ok(VmValue::Nil)
425    });
426
427    vm.register_builtin("path_join", |args, _out| {
428        let mut path = std::path::PathBuf::new();
429        for arg in args {
430            path.push(arg.display());
431        }
432        Ok(VmValue::String(Rc::from(
433            path.to_string_lossy().to_string().as_str(),
434        )))
435    });
436
437    vm.register_builtin("copy_file", |args, _out| {
438        if args.len() >= 2 {
439            let src = args[0].display();
440            let dst = args[1].display();
441            std::fs::copy(&src, &dst).map_err(|e| {
442                VmError::Thrown(VmValue::String(Rc::from(format!(
443                    "Failed to copy {src} to {dst}: {e}"
444                ))))
445            })?;
446        }
447        Ok(VmValue::Nil)
448    });
449
450    vm.register_builtin("temp_dir", |_args, _out| {
451        Ok(VmValue::String(Rc::from(
452            std::env::temp_dir().to_string_lossy().to_string().as_str(),
453        )))
454    });
455
456    vm.register_builtin("stat", |args, _out| {
457        let path = args.first().map(|a| a.display()).unwrap_or_default();
458        let metadata = std::fs::metadata(&path).map_err(|e| {
459            VmError::Thrown(VmValue::String(Rc::from(format!(
460                "Failed to stat {path}: {e}"
461            ))))
462        })?;
463        let mut info = BTreeMap::new();
464        info.insert("size".to_string(), VmValue::Int(metadata.len() as i64));
465        info.insert("is_file".to_string(), VmValue::Bool(metadata.is_file()));
466        info.insert("is_dir".to_string(), VmValue::Bool(metadata.is_dir()));
467        info.insert(
468            "readonly".to_string(),
469            VmValue::Bool(metadata.permissions().readonly()),
470        );
471        if let Ok(modified) = metadata.modified() {
472            if let Ok(dur) = modified.duration_since(std::time::UNIX_EPOCH) {
473                info.insert("modified".to_string(), VmValue::Float(dur.as_secs_f64()));
474            }
475        }
476        Ok(VmValue::Dict(Rc::new(info)))
477    });
478
479    // =========================================================================
480    // Process execution builtins
481    // =========================================================================
482
483    vm.register_builtin("exec", |args, _out| {
484        if args.is_empty() {
485            return Err(VmError::Thrown(VmValue::String(Rc::from(
486                "exec: command is required",
487            ))));
488        }
489        let cmd = args[0].display();
490        let cmd_args: Vec<String> = args[1..].iter().map(|a| a.display()).collect();
491        let output = std::process::Command::new(&cmd)
492            .args(&cmd_args)
493            .output()
494            .map_err(|e| VmError::Thrown(VmValue::String(Rc::from(format!("exec failed: {e}")))))?;
495        Ok(vm_output_to_value(output))
496    });
497
498    vm.register_builtin("shell", |args, _out| {
499        let cmd = args.first().map(|a| a.display()).unwrap_or_default();
500        if cmd.is_empty() {
501            return Err(VmError::Thrown(VmValue::String(Rc::from(
502                "shell: command string is required",
503            ))));
504        }
505        let shell = if cfg!(target_os = "windows") {
506            "cmd"
507        } else {
508            "sh"
509        };
510        let flag = if cfg!(target_os = "windows") {
511            "/C"
512        } else {
513            "-c"
514        };
515        let output = std::process::Command::new(shell)
516            .arg(flag)
517            .arg(&cmd)
518            .output()
519            .map_err(|e| {
520                VmError::Thrown(VmValue::String(Rc::from(format!("shell failed: {e}"))))
521            })?;
522        Ok(vm_output_to_value(output))
523    });
524
525    // =========================================================================
526    // Date/time builtins
527    // =========================================================================
528
529    vm.register_builtin("date_now", |_args, _out| {
530        use std::time::{SystemTime, UNIX_EPOCH};
531        let now = SystemTime::now()
532            .duration_since(UNIX_EPOCH)
533            .unwrap_or_default();
534        let total_secs = now.as_secs();
535        let (y, m, d, hour, minute, second, dow) = vm_civil_from_timestamp(total_secs);
536        let mut result = BTreeMap::new();
537        result.insert("year".to_string(), VmValue::Int(y));
538        result.insert("month".to_string(), VmValue::Int(m));
539        result.insert("day".to_string(), VmValue::Int(d));
540        result.insert("hour".to_string(), VmValue::Int(hour));
541        result.insert("minute".to_string(), VmValue::Int(minute));
542        result.insert("second".to_string(), VmValue::Int(second));
543        result.insert("weekday".to_string(), VmValue::Int(dow));
544        result.insert("timestamp".to_string(), VmValue::Float(now.as_secs_f64()));
545        Ok(VmValue::Dict(Rc::new(result)))
546    });
547
548    vm.register_builtin("date_format", |args, _out| {
549        let ts = match args.first() {
550            Some(VmValue::Float(f)) => *f,
551            Some(VmValue::Int(n)) => *n as f64,
552            Some(VmValue::Dict(map)) => map
553                .get("timestamp")
554                .and_then(|v| match v {
555                    VmValue::Float(f) => Some(*f),
556                    VmValue::Int(n) => Some(*n as f64),
557                    _ => None,
558                })
559                .unwrap_or(0.0),
560            _ => 0.0,
561        };
562        let fmt = args
563            .get(1)
564            .map(|a| a.display())
565            .unwrap_or_else(|| "%Y-%m-%d %H:%M:%S".to_string());
566
567        let (y, m, d, hour, minute, second, _dow) = vm_civil_from_timestamp(ts as u64);
568
569        let result = fmt
570            .replace("%Y", &format!("{y:04}"))
571            .replace("%m", &format!("{m:02}"))
572            .replace("%d", &format!("{d:02}"))
573            .replace("%H", &format!("{hour:02}"))
574            .replace("%M", &format!("{minute:02}"))
575            .replace("%S", &format!("{second:02}"));
576
577        Ok(VmValue::String(Rc::from(result.as_str())))
578    });
579
580    vm.register_builtin("date_parse", |args, _out| {
581        let s = args.first().map(|a| a.display()).unwrap_or_default();
582        let parts: Vec<&str> = s.split(|c: char| !c.is_ascii_digit()).collect();
583        let parts: Vec<i64> = parts.iter().filter_map(|p| p.parse().ok()).collect();
584        if parts.len() < 3 {
585            return Err(VmError::Thrown(VmValue::String(Rc::from(format!(
586                "Cannot parse date: {s}"
587            )))));
588        }
589        let (y, m, d) = (parts[0], parts[1], parts[2]);
590        let hour = parts.get(3).copied().unwrap_or(0);
591        let minute = parts.get(4).copied().unwrap_or(0);
592        let second = parts.get(5).copied().unwrap_or(0);
593
594        let (y_adj, m_adj) = if m <= 2 {
595            (y - 1, (m + 9) as u64)
596        } else {
597            (y, (m - 3) as u64)
598        };
599        let era = if y_adj >= 0 { y_adj } else { y_adj - 399 } / 400;
600        let yoe = (y_adj - era * 400) as u64;
601        let doy = (153 * m_adj + 2) / 5 + d as u64 - 1;
602        let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
603        let days = era * 146097 + doe as i64 - 719468;
604        let ts = days * 86400 + hour * 3600 + minute * 60 + second;
605        Ok(VmValue::Float(ts as f64))
606    });
607
608    // =========================================================================
609    // String formatting
610    // =========================================================================
611
612    vm.register_builtin("format", |args, _out| {
613        let template = args.first().map(|a| a.display()).unwrap_or_default();
614        let mut result = String::with_capacity(template.len());
615        let mut arg_iter = args.iter().skip(1);
616        let mut rest = template.as_str();
617        while let Some(pos) = rest.find("{}") {
618            result.push_str(&rest[..pos]);
619            if let Some(arg) = arg_iter.next() {
620                result.push_str(&arg.display());
621            } else {
622                result.push_str("{}");
623            }
624            rest = &rest[pos + 2..];
625        }
626        result.push_str(rest);
627        Ok(VmValue::String(Rc::from(result.as_str())))
628    });
629
630    // =========================================================================
631    // Standalone string function builtins
632    // =========================================================================
633
634    vm.register_builtin("trim", |args, _out| {
635        let s = args.first().map(|a| a.display()).unwrap_or_default();
636        Ok(VmValue::String(Rc::from(s.trim())))
637    });
638
639    vm.register_builtin("lowercase", |args, _out| {
640        let s = args.first().map(|a| a.display()).unwrap_or_default();
641        Ok(VmValue::String(Rc::from(s.to_lowercase().as_str())))
642    });
643
644    vm.register_builtin("uppercase", |args, _out| {
645        let s = args.first().map(|a| a.display()).unwrap_or_default();
646        Ok(VmValue::String(Rc::from(s.to_uppercase().as_str())))
647    });
648
649    vm.register_builtin("split", |args, _out| {
650        let s = args.first().map(|a| a.display()).unwrap_or_default();
651        let sep = args
652            .get(1)
653            .map(|a| a.display())
654            .unwrap_or_else(|| " ".to_string());
655        let parts: Vec<VmValue> = s
656            .split(&sep)
657            .map(|p| VmValue::String(Rc::from(p)))
658            .collect();
659        Ok(VmValue::List(Rc::new(parts)))
660    });
661
662    vm.register_builtin("starts_with", |args, _out| {
663        let s = args.first().map(|a| a.display()).unwrap_or_default();
664        let prefix = args.get(1).map(|a| a.display()).unwrap_or_default();
665        Ok(VmValue::Bool(s.starts_with(&prefix)))
666    });
667
668    vm.register_builtin("ends_with", |args, _out| {
669        let s = args.first().map(|a| a.display()).unwrap_or_default();
670        let suffix = args.get(1).map(|a| a.display()).unwrap_or_default();
671        Ok(VmValue::Bool(s.ends_with(&suffix)))
672    });
673
674    vm.register_builtin("contains", |args, _out| {
675        match args.first().unwrap_or(&VmValue::Nil) {
676            VmValue::String(s) => {
677                let substr = args.get(1).map(|a| a.display()).unwrap_or_default();
678                Ok(VmValue::Bool(s.contains(&substr)))
679            }
680            VmValue::List(items) => {
681                let target = args.get(1).unwrap_or(&VmValue::Nil);
682                Ok(VmValue::Bool(
683                    items.iter().any(|item| values_equal(item, target)),
684                ))
685            }
686            _ => Ok(VmValue::Bool(false)),
687        }
688    });
689
690    vm.register_builtin("replace", |args, _out| {
691        let s = args.first().map(|a| a.display()).unwrap_or_default();
692        let old = args.get(1).map(|a| a.display()).unwrap_or_default();
693        let new = args.get(2).map(|a| a.display()).unwrap_or_default();
694        Ok(VmValue::String(Rc::from(s.replace(&old, &new).as_str())))
695    });
696
697    vm.register_builtin("join", |args, _out| {
698        let sep = args.get(1).map(|a| a.display()).unwrap_or_default();
699        match args.first() {
700            Some(VmValue::List(items)) => {
701                let parts: Vec<String> = items.iter().map(|v| v.display()).collect();
702                Ok(VmValue::String(Rc::from(parts.join(&sep).as_str())))
703            }
704            _ => Ok(VmValue::String(Rc::from(""))),
705        }
706    });
707
708    vm.register_builtin("len", |args, _out| {
709        match args.first().unwrap_or(&VmValue::Nil) {
710            VmValue::String(s) => Ok(VmValue::Int(s.len() as i64)),
711            VmValue::List(items) => Ok(VmValue::Int(items.len() as i64)),
712            VmValue::Dict(map) => Ok(VmValue::Int(map.len() as i64)),
713            _ => Ok(VmValue::Int(0)),
714        }
715    });
716
717    vm.register_builtin("substring", |args, _out| {
718        let s = args.first().map(|a| a.display()).unwrap_or_default();
719        let start = args.get(1).and_then(|a| a.as_int()).unwrap_or(0) as usize;
720        let start = start.min(s.len());
721        match args.get(2).and_then(|a| a.as_int()) {
722            Some(length) => {
723                let length = (length as usize).min(s.len() - start);
724                Ok(VmValue::String(Rc::from(&s[start..start + length])))
725            }
726            None => Ok(VmValue::String(Rc::from(&s[start..]))),
727        }
728    });
729
730    // =========================================================================
731    // Path builtins
732    // =========================================================================
733
734    vm.register_builtin("dirname", |args, _out| {
735        let path = args.first().map(|a| a.display()).unwrap_or_default();
736        let p = std::path::Path::new(&path);
737        match p.parent() {
738            Some(parent) => Ok(VmValue::String(Rc::from(parent.to_string_lossy().as_ref()))),
739            None => Ok(VmValue::String(Rc::from(""))),
740        }
741    });
742
743    vm.register_builtin("basename", |args, _out| {
744        let path = args.first().map(|a| a.display()).unwrap_or_default();
745        let p = std::path::Path::new(&path);
746        match p.file_name() {
747            Some(name) => Ok(VmValue::String(Rc::from(name.to_string_lossy().as_ref()))),
748            None => Ok(VmValue::String(Rc::from(""))),
749        }
750    });
751
752    vm.register_builtin("extname", |args, _out| {
753        let path = args.first().map(|a| a.display()).unwrap_or_default();
754        let p = std::path::Path::new(&path);
755        match p.extension() {
756            Some(ext) => Ok(VmValue::String(Rc::from(
757                format!(".{}", ext.to_string_lossy()).as_str(),
758            ))),
759            None => Ok(VmValue::String(Rc::from(""))),
760        }
761    });
762
763    // =========================================================================
764    // Prompt template rendering
765    // =========================================================================
766
767    vm.register_builtin("render", |args, _out| {
768        let path = args.first().map(|a| a.display()).unwrap_or_default();
769        let template = std::fs::read_to_string(&path).map_err(|e| {
770            VmError::Thrown(VmValue::String(Rc::from(format!(
771                "Failed to read template {path}: {e}"
772            ))))
773        })?;
774        if let Some(bindings) = args.get(1).and_then(|a| a.as_dict()) {
775            let mut result = template;
776            for (key, val) in bindings.iter() {
777                result = result.replace(&format!("{{{{{key}}}}}"), &val.display());
778            }
779            Ok(VmValue::String(Rc::from(result)))
780        } else {
781            Ok(VmValue::String(Rc::from(template)))
782        }
783    });
784
785    // =========================================================================
786    // Logging builtins
787    // =========================================================================
788
789    vm.register_builtin("log_debug", |args, out| {
790        vm_write_log("debug", 0, args, out);
791        Ok(VmValue::Nil)
792    });
793
794    vm.register_builtin("log_info", |args, out| {
795        vm_write_log("info", 1, args, out);
796        Ok(VmValue::Nil)
797    });
798
799    vm.register_builtin("log_warn", |args, out| {
800        vm_write_log("warn", 2, args, out);
801        Ok(VmValue::Nil)
802    });
803
804    vm.register_builtin("log_error", |args, out| {
805        vm_write_log("error", 3, args, out);
806        Ok(VmValue::Nil)
807    });
808
809    vm.register_builtin("log_set_level", |args, _out| {
810        let level_str = args.first().map(|a| a.display()).unwrap_or_default();
811        match vm_level_to_u8(&level_str) {
812            Some(n) => {
813                VM_MIN_LOG_LEVEL.store(n, Ordering::Relaxed);
814                Ok(VmValue::Nil)
815            }
816            None => Err(VmError::Thrown(VmValue::String(Rc::from(format!(
817                "log_set_level: invalid level '{}'. Expected debug, info, warn, or error",
818                level_str
819            ))))),
820        }
821    });
822
823    // =========================================================================
824    // Tracing builtins
825    // =========================================================================
826
827    vm.register_builtin("trace_start", |args, _out| {
828        use rand::Rng;
829        let name = args.first().map(|a| a.display()).unwrap_or_default();
830        let trace_id = VM_TRACE_STACK.with(|stack| {
831            stack
832                .borrow()
833                .last()
834                .map(|t| t.trace_id.clone())
835                .unwrap_or_else(|| {
836                    let val: u32 = rand::thread_rng().gen();
837                    format!("{val:08x}")
838                })
839        });
840        let span_id = {
841            let val: u32 = rand::thread_rng().gen();
842            format!("{val:08x}")
843        };
844        let start_ms = std::time::SystemTime::now()
845            .duration_since(std::time::UNIX_EPOCH)
846            .unwrap_or_default()
847            .as_millis() as i64;
848
849        VM_TRACE_STACK.with(|stack| {
850            stack.borrow_mut().push(VmTraceContext {
851                trace_id: trace_id.clone(),
852                span_id: span_id.clone(),
853            });
854        });
855
856        let mut span = BTreeMap::new();
857        span.insert(
858            "trace_id".to_string(),
859            VmValue::String(Rc::from(trace_id.as_str())),
860        );
861        span.insert(
862            "span_id".to_string(),
863            VmValue::String(Rc::from(span_id.as_str())),
864        );
865        span.insert("name".to_string(), VmValue::String(Rc::from(name.as_str())));
866        span.insert("start_ms".to_string(), VmValue::Int(start_ms));
867        Ok(VmValue::Dict(Rc::new(span)))
868    });
869
870    vm.register_builtin("trace_end", |args, out| {
871        let span = match args.first() {
872            Some(VmValue::Dict(d)) => d,
873            _ => {
874                return Err(VmError::Thrown(VmValue::String(Rc::from(
875                    "trace_end: argument must be a span dict from trace_start",
876                ))));
877            }
878        };
879
880        let end_ms = std::time::SystemTime::now()
881            .duration_since(std::time::UNIX_EPOCH)
882            .unwrap_or_default()
883            .as_millis() as i64;
884
885        let start_ms = span
886            .get("start_ms")
887            .and_then(|v| v.as_int())
888            .unwrap_or(end_ms);
889        let duration_ms = end_ms - start_ms;
890        let name = span.get("name").map(|v| v.display()).unwrap_or_default();
891        let trace_id = span
892            .get("trace_id")
893            .map(|v| v.display())
894            .unwrap_or_default();
895        let span_id = span.get("span_id").map(|v| v.display()).unwrap_or_default();
896
897        VM_TRACE_STACK.with(|stack| {
898            stack.borrow_mut().pop();
899        });
900
901        let level_num = 1_u8;
902        if level_num >= VM_MIN_LOG_LEVEL.load(Ordering::Relaxed) {
903            let mut fields = BTreeMap::new();
904            fields.insert(
905                "trace_id".to_string(),
906                VmValue::String(Rc::from(trace_id.as_str())),
907            );
908            fields.insert(
909                "span_id".to_string(),
910                VmValue::String(Rc::from(span_id.as_str())),
911            );
912            fields.insert("name".to_string(), VmValue::String(Rc::from(name.as_str())));
913            fields.insert("duration_ms".to_string(), VmValue::Int(duration_ms));
914            let line = vm_build_log_line("info", "span_end", Some(&fields));
915            out.push_str(&line);
916        }
917
918        Ok(VmValue::Nil)
919    });
920
921    vm.register_builtin("trace_id", |_args, _out| {
922        let id = VM_TRACE_STACK.with(|stack| stack.borrow().last().map(|t| t.trace_id.clone()));
923        match id {
924            Some(trace_id) => Ok(VmValue::String(Rc::from(trace_id.as_str()))),
925            None => Ok(VmValue::Nil),
926        }
927    });
928
929    // =========================================================================
930    // Tool registry builtins
931    // =========================================================================
932
933    vm.register_builtin("tool_registry", |_args, _out| {
934        let mut registry = BTreeMap::new();
935        registry.insert(
936            "_type".to_string(),
937            VmValue::String(Rc::from("tool_registry")),
938        );
939        registry.insert("tools".to_string(), VmValue::List(Rc::new(Vec::new())));
940        Ok(VmValue::Dict(Rc::new(registry)))
941    });
942
943    vm.register_builtin("tool_add", |args, _out| {
944        if args.len() < 4 {
945            return Err(VmError::Thrown(VmValue::String(Rc::from(
946                "tool_add: requires registry, name, description, and handler",
947            ))));
948        }
949
950        let registry = match &args[0] {
951            VmValue::Dict(map) => (**map).clone(),
952            _ => {
953                return Err(VmError::Thrown(VmValue::String(Rc::from(
954                    "tool_add: first argument must be a tool registry",
955                ))));
956            }
957        };
958
959        match registry.get("_type") {
960            Some(VmValue::String(t)) if &**t == "tool_registry" => {}
961            _ => {
962                return Err(VmError::Thrown(VmValue::String(Rc::from(
963                    "tool_add: first argument must be a tool registry",
964                ))));
965            }
966        }
967
968        let name = args[1].display();
969        let description = args[2].display();
970        let handler = args[3].clone();
971        let parameters = if args.len() > 4 {
972            args[4].clone()
973        } else {
974            VmValue::Dict(Rc::new(BTreeMap::new()))
975        };
976
977        let mut tool_entry = BTreeMap::new();
978        tool_entry.insert("name".to_string(), VmValue::String(Rc::from(name.as_str())));
979        tool_entry.insert(
980            "description".to_string(),
981            VmValue::String(Rc::from(description.as_str())),
982        );
983        tool_entry.insert("handler".to_string(), handler);
984        tool_entry.insert("parameters".to_string(), parameters);
985
986        let mut tools: Vec<VmValue> = match registry.get("tools") {
987            Some(VmValue::List(list)) => list
988                .iter()
989                .filter(|t| {
990                    if let VmValue::Dict(e) = t {
991                        e.get("name").map(|v| v.display()).as_deref() != Some(name.as_str())
992                    } else {
993                        true
994                    }
995                })
996                .cloned()
997                .collect(),
998            _ => Vec::new(),
999        };
1000        tools.push(VmValue::Dict(Rc::new(tool_entry)));
1001
1002        let mut new_registry = registry;
1003        new_registry.insert("tools".to_string(), VmValue::List(Rc::new(tools)));
1004        Ok(VmValue::Dict(Rc::new(new_registry)))
1005    });
1006
1007    vm.register_builtin("tool_list", |args, _out| {
1008        let registry = match args.first() {
1009            Some(VmValue::Dict(map)) => map,
1010            _ => {
1011                return Err(VmError::Thrown(VmValue::String(Rc::from(
1012                    "tool_list: requires a tool registry",
1013                ))));
1014            }
1015        };
1016        vm_validate_registry("tool_list", registry)?;
1017
1018        let tools = vm_get_tools(registry);
1019        let mut result = Vec::new();
1020        for tool in tools {
1021            if let VmValue::Dict(entry) = tool {
1022                let mut desc = BTreeMap::new();
1023                if let Some(name) = entry.get("name") {
1024                    desc.insert("name".to_string(), name.clone());
1025                }
1026                if let Some(description) = entry.get("description") {
1027                    desc.insert("description".to_string(), description.clone());
1028                }
1029                if let Some(parameters) = entry.get("parameters") {
1030                    desc.insert("parameters".to_string(), parameters.clone());
1031                }
1032                result.push(VmValue::Dict(Rc::new(desc)));
1033            }
1034        }
1035        Ok(VmValue::List(Rc::new(result)))
1036    });
1037
1038    vm.register_builtin("tool_find", |args, _out| {
1039        if args.len() < 2 {
1040            return Err(VmError::Thrown(VmValue::String(Rc::from(
1041                "tool_find: requires registry and name",
1042            ))));
1043        }
1044
1045        let registry = match &args[0] {
1046            VmValue::Dict(map) => map,
1047            _ => {
1048                return Err(VmError::Thrown(VmValue::String(Rc::from(
1049                    "tool_find: first argument must be a tool registry",
1050                ))));
1051            }
1052        };
1053        vm_validate_registry("tool_find", registry)?;
1054
1055        let target_name = args[1].display();
1056        let tools = vm_get_tools(registry);
1057
1058        for tool in tools {
1059            if let VmValue::Dict(entry) = tool {
1060                if let Some(VmValue::String(name)) = entry.get("name") {
1061                    if &**name == target_name.as_str() {
1062                        return Ok(tool.clone());
1063                    }
1064                }
1065            }
1066        }
1067        Ok(VmValue::Nil)
1068    });
1069
1070    vm.register_builtin("tool_describe", |args, _out| {
1071        let registry = match args.first() {
1072            Some(VmValue::Dict(map)) => map,
1073            _ => {
1074                return Err(VmError::Thrown(VmValue::String(Rc::from(
1075                    "tool_describe: requires a tool registry",
1076                ))));
1077            }
1078        };
1079        vm_validate_registry("tool_describe", registry)?;
1080
1081        let tools = vm_get_tools(registry);
1082
1083        if tools.is_empty() {
1084            return Ok(VmValue::String(Rc::from("Available tools:\n(none)")));
1085        }
1086
1087        let mut tool_infos: Vec<(String, String, String)> = Vec::new();
1088        for tool in tools {
1089            if let VmValue::Dict(entry) = tool {
1090                let name = entry.get("name").map(|v| v.display()).unwrap_or_default();
1091                let description = entry
1092                    .get("description")
1093                    .map(|v| v.display())
1094                    .unwrap_or_default();
1095                let params_str = vm_format_parameters(entry.get("parameters"));
1096                tool_infos.push((name, params_str, description));
1097            }
1098        }
1099
1100        tool_infos.sort_by(|a, b| a.0.cmp(&b.0));
1101
1102        let mut lines = vec!["Available tools:".to_string()];
1103        for (name, params, desc) in &tool_infos {
1104            lines.push(format!("- {name}({params}): {desc}"));
1105        }
1106
1107        Ok(VmValue::String(Rc::from(lines.join("\n").as_str())))
1108    });
1109
1110    vm.register_builtin("tool_remove", |args, _out| {
1111        if args.len() < 2 {
1112            return Err(VmError::Thrown(VmValue::String(Rc::from(
1113                "tool_remove: requires registry and name",
1114            ))));
1115        }
1116
1117        let registry = match &args[0] {
1118            VmValue::Dict(map) => (**map).clone(),
1119            _ => {
1120                return Err(VmError::Thrown(VmValue::String(Rc::from(
1121                    "tool_remove: first argument must be a tool registry",
1122                ))));
1123            }
1124        };
1125        vm_validate_registry("tool_remove", &registry)?;
1126
1127        let target_name = args[1].display();
1128
1129        let tools = match registry.get("tools") {
1130            Some(VmValue::List(list)) => (**list).clone(),
1131            _ => Vec::new(),
1132        };
1133
1134        let filtered: Vec<VmValue> = tools
1135            .into_iter()
1136            .filter(|tool| {
1137                if let VmValue::Dict(entry) = tool {
1138                    if let Some(VmValue::String(name)) = entry.get("name") {
1139                        return &**name != target_name.as_str();
1140                    }
1141                }
1142                true
1143            })
1144            .collect();
1145
1146        let mut new_registry = registry;
1147        new_registry.insert("tools".to_string(), VmValue::List(Rc::new(filtered)));
1148        Ok(VmValue::Dict(Rc::new(new_registry)))
1149    });
1150
1151    vm.register_builtin("tool_count", |args, _out| {
1152        let registry = match args.first() {
1153            Some(VmValue::Dict(map)) => map,
1154            _ => {
1155                return Err(VmError::Thrown(VmValue::String(Rc::from(
1156                    "tool_count: requires a tool registry",
1157                ))));
1158            }
1159        };
1160        vm_validate_registry("tool_count", registry)?;
1161        let count = vm_get_tools(registry).len();
1162        Ok(VmValue::Int(count as i64))
1163    });
1164
1165    vm.register_builtin("tool_schema", |args, _out| {
1166        let registry = match args.first() {
1167            Some(VmValue::Dict(map)) => {
1168                vm_validate_registry("tool_schema", map)?;
1169                map
1170            }
1171            _ => {
1172                return Err(VmError::Thrown(VmValue::String(Rc::from(
1173                    "tool_schema: requires a tool registry",
1174                ))));
1175            }
1176        };
1177
1178        let components = args.get(1).and_then(|v| v.as_dict()).cloned();
1179
1180        let tools = match registry.get("tools") {
1181            Some(VmValue::List(list)) => list,
1182            _ => return Ok(VmValue::Dict(Rc::new(vm_build_empty_schema()))),
1183        };
1184
1185        let mut tool_schemas = Vec::new();
1186        for tool in tools.iter() {
1187            if let VmValue::Dict(entry) = tool {
1188                let name = entry.get("name").map(|v| v.display()).unwrap_or_default();
1189                let description = entry
1190                    .get("description")
1191                    .map(|v| v.display())
1192                    .unwrap_or_default();
1193
1194                let input_schema =
1195                    vm_build_input_schema(entry.get("parameters"), components.as_ref());
1196
1197                let mut tool_def = BTreeMap::new();
1198                tool_def.insert("name".to_string(), VmValue::String(Rc::from(name.as_str())));
1199                tool_def.insert(
1200                    "description".to_string(),
1201                    VmValue::String(Rc::from(description.as_str())),
1202                );
1203                tool_def.insert("inputSchema".to_string(), input_schema);
1204                tool_schemas.push(VmValue::Dict(Rc::new(tool_def)));
1205            }
1206        }
1207
1208        let mut schema = BTreeMap::new();
1209        schema.insert(
1210            "schema_version".to_string(),
1211            VmValue::String(Rc::from("harn-tools/1.0")),
1212        );
1213
1214        if let Some(comps) = &components {
1215            let mut comp_wrapper = BTreeMap::new();
1216            comp_wrapper.insert("schemas".to_string(), VmValue::Dict(Rc::new(comps.clone())));
1217            schema.insert(
1218                "components".to_string(),
1219                VmValue::Dict(Rc::new(comp_wrapper)),
1220            );
1221        }
1222
1223        schema.insert("tools".to_string(), VmValue::List(Rc::new(tool_schemas)));
1224        Ok(VmValue::Dict(Rc::new(schema)))
1225    });
1226
1227    vm.register_builtin("tool_parse_call", |args, _out| {
1228        let text = args.first().map(|a| a.display()).unwrap_or_default();
1229
1230        let mut results = Vec::new();
1231        let mut search_from = 0;
1232
1233        while let Some(start) = text[search_from..].find("<tool_call>") {
1234            let abs_start = search_from + start + "<tool_call>".len();
1235            if let Some(end) = text[abs_start..].find("</tool_call>") {
1236                let json_str = text[abs_start..abs_start + end].trim();
1237                if let Ok(jv) = serde_json::from_str::<serde_json::Value>(json_str) {
1238                    results.push(json_to_vm_value(&jv));
1239                }
1240                search_from = abs_start + end + "</tool_call>".len();
1241            } else {
1242                break;
1243            }
1244        }
1245
1246        Ok(VmValue::List(Rc::new(results)))
1247    });
1248
1249    vm.register_builtin("tool_format_result", |args, _out| {
1250        if args.len() < 2 {
1251            return Err(VmError::Thrown(VmValue::String(Rc::from(
1252                "tool_format_result: requires name and result",
1253            ))));
1254        }
1255        let name = args[0].display();
1256        let result = args[1].display();
1257
1258        let json_name = vm_escape_json_str(&name);
1259        let json_result = vm_escape_json_str(&result);
1260        Ok(VmValue::String(Rc::from(
1261            format!(
1262                "<tool_result>{{\"name\": \"{json_name}\", \"result\": \"{json_result}\"}}</tool_result>"
1263            )
1264            .as_str(),
1265        )))
1266    });
1267
1268    vm.register_builtin("tool_prompt", |args, _out| {
1269        let registry = match args.first() {
1270            Some(VmValue::Dict(map)) => {
1271                vm_validate_registry("tool_prompt", map)?;
1272                map
1273            }
1274            _ => {
1275                return Err(VmError::Thrown(VmValue::String(Rc::from(
1276                    "tool_prompt: requires a tool registry",
1277                ))));
1278            }
1279        };
1280
1281        let tools = match registry.get("tools") {
1282            Some(VmValue::List(list)) => list,
1283            _ => {
1284                return Ok(VmValue::String(Rc::from("No tools are available.")));
1285            }
1286        };
1287
1288        if tools.is_empty() {
1289            return Ok(VmValue::String(Rc::from("No tools are available.")));
1290        }
1291
1292        let mut prompt = String::from("# Available Tools\n\n");
1293        prompt.push_str("You have access to the following tools. To use a tool, output a tool call in this exact format:\n\n");
1294        prompt.push_str("<tool_call>{\"name\": \"tool_name\", \"arguments\": {\"param\": \"value\"}}</tool_call>\n\n");
1295        prompt.push_str("You may make multiple tool calls in a single response. Wait for tool results before proceeding.\n\n");
1296        prompt.push_str("## Tools\n\n");
1297
1298        let mut tool_infos: Vec<(&BTreeMap<String, VmValue>, String)> = Vec::new();
1299        for tool in tools.iter() {
1300            if let VmValue::Dict(entry) = tool {
1301                let name = entry.get("name").map(|v| v.display()).unwrap_or_default();
1302                tool_infos.push((entry, name));
1303            }
1304        }
1305        tool_infos.sort_by(|a, b| a.1.cmp(&b.1));
1306
1307        for (entry, name) in &tool_infos {
1308            let description = entry
1309                .get("description")
1310                .map(|v| v.display())
1311                .unwrap_or_default();
1312            let params_str = vm_format_parameters(entry.get("parameters"));
1313
1314            prompt.push_str(&format!("### {name}\n"));
1315            prompt.push_str(&format!("{description}\n"));
1316            if !params_str.is_empty() {
1317                prompt.push_str(&format!("Parameters: {params_str}\n"));
1318            }
1319            prompt.push('\n');
1320        }
1321
1322        Ok(VmValue::String(Rc::from(prompt.trim_end())))
1323    });
1324
1325    // =========================================================================
1326    // Channel builtins (sync)
1327    // =========================================================================
1328
1329    vm.register_builtin("channel", |args, _out| {
1330        let name = args
1331            .first()
1332            .map(|a| a.display())
1333            .unwrap_or_else(|| "default".to_string());
1334        let capacity = args.get(1).and_then(|a| a.as_int()).unwrap_or(256) as usize;
1335        let capacity = capacity.max(1);
1336        let (tx, rx) = tokio::sync::mpsc::channel(capacity);
1337        #[allow(clippy::arc_with_non_send_sync)]
1338        Ok(VmValue::Channel(VmChannelHandle {
1339            name,
1340            sender: Arc::new(tx),
1341            receiver: Arc::new(tokio::sync::Mutex::new(rx)),
1342            closed: Arc::new(AtomicBool::new(false)),
1343        }))
1344    });
1345
1346    vm.register_builtin("close_channel", |args, _out| {
1347        if args.is_empty() {
1348            return Err(VmError::Thrown(VmValue::String(Rc::from(
1349                "close_channel: requires a channel",
1350            ))));
1351        }
1352        if let VmValue::Channel(ch) = &args[0] {
1353            ch.closed.store(true, Ordering::SeqCst);
1354            Ok(VmValue::Nil)
1355        } else {
1356            Err(VmError::Thrown(VmValue::String(Rc::from(
1357                "close_channel: first argument must be a channel",
1358            ))))
1359        }
1360    });
1361
1362    vm.register_builtin("try_receive", |args, _out| {
1363        if args.is_empty() {
1364            return Err(VmError::Thrown(VmValue::String(Rc::from(
1365                "try_receive: requires a channel",
1366            ))));
1367        }
1368        if let VmValue::Channel(ch) = &args[0] {
1369            match ch.receiver.try_lock() {
1370                Ok(mut rx) => match rx.try_recv() {
1371                    Ok(val) => Ok(val),
1372                    Err(_) => Ok(VmValue::Nil),
1373                },
1374                Err(_) => Ok(VmValue::Nil),
1375            }
1376        } else {
1377            Err(VmError::Thrown(VmValue::String(Rc::from(
1378                "try_receive: first argument must be a channel",
1379            ))))
1380        }
1381    });
1382
1383    // =========================================================================
1384    // Atomic builtins
1385    // =========================================================================
1386
1387    vm.register_builtin("atomic", |args, _out| {
1388        let initial = match args.first() {
1389            Some(VmValue::Int(n)) => *n,
1390            Some(VmValue::Float(f)) => *f as i64,
1391            Some(VmValue::Bool(b)) => {
1392                if *b {
1393                    1
1394                } else {
1395                    0
1396                }
1397            }
1398            _ => 0,
1399        };
1400        Ok(VmValue::Atomic(VmAtomicHandle {
1401            value: Arc::new(AtomicI64::new(initial)),
1402        }))
1403    });
1404
1405    vm.register_builtin("atomic_get", |args, _out| {
1406        if let Some(VmValue::Atomic(a)) = args.first() {
1407            Ok(VmValue::Int(a.value.load(Ordering::SeqCst)))
1408        } else {
1409            Ok(VmValue::Nil)
1410        }
1411    });
1412
1413    vm.register_builtin("atomic_set", |args, _out| {
1414        if args.len() >= 2 {
1415            if let (VmValue::Atomic(a), Some(val)) = (&args[0], args[1].as_int()) {
1416                let old = a.value.swap(val, Ordering::SeqCst);
1417                return Ok(VmValue::Int(old));
1418            }
1419        }
1420        Ok(VmValue::Nil)
1421    });
1422
1423    vm.register_builtin("atomic_add", |args, _out| {
1424        if args.len() >= 2 {
1425            if let (VmValue::Atomic(a), Some(delta)) = (&args[0], args[1].as_int()) {
1426                let prev = a.value.fetch_add(delta, Ordering::SeqCst);
1427                return Ok(VmValue::Int(prev));
1428            }
1429        }
1430        Ok(VmValue::Nil)
1431    });
1432
1433    vm.register_builtin("atomic_cas", |args, _out| {
1434        if args.len() >= 3 {
1435            if let (VmValue::Atomic(a), Some(expected), Some(new_val)) =
1436                (&args[0], args[1].as_int(), args[2].as_int())
1437            {
1438                let result =
1439                    a.value
1440                        .compare_exchange(expected, new_val, Ordering::SeqCst, Ordering::SeqCst);
1441                return Ok(VmValue::Bool(result.is_ok()));
1442            }
1443        }
1444        Ok(VmValue::Bool(false))
1445    });
1446
1447    // =========================================================================
1448    // Async builtins
1449    // =========================================================================
1450
1451    // sleep(ms)
1452    vm.register_async_builtin("sleep", |args| async move {
1453        let ms = match args.first() {
1454            Some(VmValue::Duration(ms)) => *ms,
1455            Some(VmValue::Int(n)) => *n as u64,
1456            _ => 0,
1457        };
1458        if ms > 0 {
1459            tokio::time::sleep(tokio::time::Duration::from_millis(ms)).await;
1460        }
1461        Ok(VmValue::Nil)
1462    });
1463
1464    // send(channel, value)
1465    vm.register_async_builtin("send", |args| async move {
1466        if args.len() < 2 {
1467            return Err(VmError::Thrown(VmValue::String(Rc::from(
1468                "send: requires channel and value",
1469            ))));
1470        }
1471        if let VmValue::Channel(ch) = &args[0] {
1472            if ch.closed.load(Ordering::SeqCst) {
1473                return Ok(VmValue::Bool(false));
1474            }
1475            let val = args[1].clone();
1476            match ch.sender.send(val).await {
1477                Ok(()) => Ok(VmValue::Bool(true)),
1478                Err(_) => Ok(VmValue::Bool(false)),
1479            }
1480        } else {
1481            Err(VmError::Thrown(VmValue::String(Rc::from(
1482                "send: first argument must be a channel",
1483            ))))
1484        }
1485    });
1486
1487    // receive(channel)
1488    vm.register_async_builtin("receive", |args| async move {
1489        if args.is_empty() {
1490            return Err(VmError::Thrown(VmValue::String(Rc::from(
1491                "receive: requires a channel",
1492            ))));
1493        }
1494        if let VmValue::Channel(ch) = &args[0] {
1495            if ch.closed.load(Ordering::SeqCst) {
1496                let mut rx = ch.receiver.lock().await;
1497                return match rx.try_recv() {
1498                    Ok(val) => Ok(val),
1499                    Err(_) => Ok(VmValue::Nil),
1500                };
1501            }
1502            let mut rx = ch.receiver.lock().await;
1503            match rx.recv().await {
1504                Some(val) => Ok(val),
1505                None => Ok(VmValue::Nil),
1506            }
1507        } else {
1508            Err(VmError::Thrown(VmValue::String(Rc::from(
1509                "receive: first argument must be a channel",
1510            ))))
1511        }
1512    });
1513
1514    // select(channel1, channel2, ...)
1515    vm.register_async_builtin("select", |args| async move {
1516        if args.is_empty() {
1517            return Err(VmError::Thrown(VmValue::String(Rc::from(
1518                "select: requires at least one channel",
1519            ))));
1520        }
1521
1522        let mut channels: Vec<&VmChannelHandle> = Vec::new();
1523        for arg in &args {
1524            if let VmValue::Channel(ch) = arg {
1525                channels.push(ch);
1526            } else {
1527                return Err(VmError::Thrown(VmValue::String(Rc::from(
1528                    "select: all arguments must be channels",
1529                ))));
1530            }
1531        }
1532
1533        loop {
1534            let mut all_closed = true;
1535            for (i, ch) in channels.iter().enumerate() {
1536                if let Ok(mut rx) = ch.receiver.try_lock() {
1537                    match rx.try_recv() {
1538                        Ok(val) => {
1539                            let mut result = BTreeMap::new();
1540                            result.insert("index".to_string(), VmValue::Int(i as i64));
1541                            result.insert("value".to_string(), val);
1542                            result.insert(
1543                                "channel".to_string(),
1544                                VmValue::String(Rc::from(ch.name.as_str())),
1545                            );
1546                            return Ok(VmValue::Dict(Rc::new(result)));
1547                        }
1548                        Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {
1549                            all_closed = false;
1550                        }
1551                        Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {}
1552                    }
1553                }
1554            }
1555            if all_closed {
1556                return Ok(VmValue::Nil);
1557            }
1558            tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
1559        }
1560    });
1561
1562    // =========================================================================
1563    // JSON validation and extraction builtins
1564    // =========================================================================
1565
1566    vm.register_builtin("json_validate", |args, _out| {
1567        if args.len() < 2 {
1568            return Err(VmError::Thrown(VmValue::String(Rc::from(
1569                "json_validate requires 2 arguments: data and schema",
1570            ))));
1571        }
1572        let data = &args[0];
1573        let schema = &args[1];
1574        let schema_dict = match schema.as_dict() {
1575            Some(d) => d,
1576            None => {
1577                return Err(VmError::Thrown(VmValue::String(Rc::from(
1578                    "json_validate: schema must be a dict",
1579                ))));
1580            }
1581        };
1582        let mut errors = Vec::new();
1583        validate_value(data, schema_dict, "", &mut errors);
1584        if errors.is_empty() {
1585            Ok(VmValue::Bool(true))
1586        } else {
1587            Err(VmError::Thrown(VmValue::String(Rc::from(
1588                errors.join("; "),
1589            ))))
1590        }
1591    });
1592
1593    vm.register_builtin("json_extract", |args, _out| {
1594        if args.is_empty() {
1595            return Err(VmError::Thrown(VmValue::String(Rc::from(
1596                "json_extract requires at least 1 argument: text",
1597            ))));
1598        }
1599        let text = args[0].display();
1600        let key = args.get(1).map(|a| a.display());
1601
1602        // Extract JSON from text that may contain markdown code fences
1603        let json_str = extract_json_from_text(&text);
1604        let parsed = match serde_json::from_str::<serde_json::Value>(&json_str) {
1605            Ok(jv) => json_to_vm_value(&jv),
1606            Err(e) => {
1607                return Err(VmError::Thrown(VmValue::String(Rc::from(format!(
1608                    "json_extract: failed to parse JSON: {e}"
1609                )))));
1610            }
1611        };
1612
1613        match key {
1614            Some(k) => match &parsed {
1615                VmValue::Dict(map) => match map.get(&k) {
1616                    Some(val) => Ok(val.clone()),
1617                    None => Err(VmError::Thrown(VmValue::String(Rc::from(format!(
1618                        "json_extract: key '{}' not found",
1619                        k
1620                    ))))),
1621                },
1622                _ => Err(VmError::Thrown(VmValue::String(Rc::from(
1623                    "json_extract: parsed value is not a dict, cannot extract key",
1624                )))),
1625            },
1626            None => Ok(parsed),
1627        }
1628    });
1629
1630    // =========================================================================
1631    // HTTP and LLM builtins (registered from separate modules)
1632    // =========================================================================
1633
1634    register_http_builtins(vm);
1635    register_llm_builtins(vm);
1636    register_mcp_builtins(vm);
1637}
1638
1639// =============================================================================
1640// JSON helpers
1641// =============================================================================
1642
1643pub(crate) fn escape_json_string_vm(s: &str) -> String {
1644    let mut out = String::with_capacity(s.len() + 2);
1645    out.push('"');
1646    for ch in s.chars() {
1647        match ch {
1648            '"' => out.push_str("\\\""),
1649            '\\' => out.push_str("\\\\"),
1650            '\n' => out.push_str("\\n"),
1651            '\r' => out.push_str("\\r"),
1652            '\t' => out.push_str("\\t"),
1653            c if c.is_control() => {
1654                out.push_str(&format!("\\u{:04x}", c as u32));
1655            }
1656            c => out.push(c),
1657        }
1658    }
1659    out.push('"');
1660    out
1661}
1662
1663pub(crate) fn vm_value_to_json(val: &VmValue) -> String {
1664    match val {
1665        VmValue::String(s) => escape_json_string_vm(s),
1666        VmValue::Int(n) => n.to_string(),
1667        VmValue::Float(n) => n.to_string(),
1668        VmValue::Bool(b) => b.to_string(),
1669        VmValue::Nil => "null".to_string(),
1670        VmValue::List(items) => {
1671            let inner: Vec<String> = items.iter().map(vm_value_to_json).collect();
1672            format!("[{}]", inner.join(","))
1673        }
1674        VmValue::Dict(map) => {
1675            let inner: Vec<String> = map
1676                .iter()
1677                .map(|(k, v)| format!("{}:{}", escape_json_string_vm(k), vm_value_to_json(v)))
1678                .collect();
1679            format!("{{{}}}", inner.join(","))
1680        }
1681        _ => "null".to_string(),
1682    }
1683}
1684
1685pub(crate) fn json_to_vm_value(jv: &serde_json::Value) -> VmValue {
1686    match jv {
1687        serde_json::Value::Null => VmValue::Nil,
1688        serde_json::Value::Bool(b) => VmValue::Bool(*b),
1689        serde_json::Value::Number(n) => {
1690            if let Some(i) = n.as_i64() {
1691                VmValue::Int(i)
1692            } else {
1693                VmValue::Float(n.as_f64().unwrap_or(0.0))
1694            }
1695        }
1696        serde_json::Value::String(s) => VmValue::String(Rc::from(s.as_str())),
1697        serde_json::Value::Array(arr) => {
1698            VmValue::List(Rc::new(arr.iter().map(json_to_vm_value).collect()))
1699        }
1700        serde_json::Value::Object(map) => {
1701            let mut m = BTreeMap::new();
1702            for (k, v) in map {
1703                m.insert(k.clone(), json_to_vm_value(v));
1704            }
1705            VmValue::Dict(Rc::new(m))
1706        }
1707    }
1708}
1709
1710// =============================================================================
1711// Helper: validate a VmValue against a schema dict
1712// =============================================================================
1713
1714fn validate_value(
1715    value: &VmValue,
1716    schema: &BTreeMap<String, VmValue>,
1717    path: &str,
1718    errors: &mut Vec<String>,
1719) {
1720    // Check "type" constraint
1721    if let Some(VmValue::String(expected_type)) = schema.get("type") {
1722        let actual_type = value.type_name();
1723        let type_str: &str = expected_type;
1724        if type_str != "any" && actual_type != type_str {
1725            let location = if path.is_empty() {
1726                "root".to_string()
1727            } else {
1728                path.to_string()
1729            };
1730            errors.push(format!(
1731                "at {}: expected type '{}', got '{}'",
1732                location, type_str, actual_type
1733            ));
1734            return; // No point checking further if type is wrong
1735        }
1736    }
1737
1738    // Check "required" keys (only for dicts)
1739    if let Some(VmValue::List(required_keys)) = schema.get("required") {
1740        if let VmValue::Dict(map) = value {
1741            for key_val in required_keys.iter() {
1742                let key = key_val.display();
1743                if !map.contains_key(&key) {
1744                    let location = if path.is_empty() {
1745                        "root".to_string()
1746                    } else {
1747                        path.to_string()
1748                    };
1749                    errors.push(format!("at {}: missing required key '{}'", location, key));
1750                }
1751            }
1752        }
1753    }
1754
1755    // Check "properties" (only for dicts)
1756    if let Some(VmValue::Dict(prop_schemas)) = schema.get("properties") {
1757        if let VmValue::Dict(map) = value {
1758            for (key, prop_schema) in prop_schemas.iter() {
1759                if let Some(prop_value) = map.get(key) {
1760                    if let Some(prop_schema_dict) = prop_schema.as_dict() {
1761                        let child_path = if path.is_empty() {
1762                            key.clone()
1763                        } else {
1764                            format!("{}.{}", path, key)
1765                        };
1766                        validate_value(prop_value, prop_schema_dict, &child_path, errors);
1767                    }
1768                }
1769            }
1770        }
1771    }
1772
1773    // Check "items" (only for lists)
1774    if let Some(VmValue::Dict(item_schema)) = schema.get("items") {
1775        if let VmValue::List(items) = value {
1776            for (i, item) in items.iter().enumerate() {
1777                let child_path = if path.is_empty() {
1778                    format!("[{}]", i)
1779                } else {
1780                    format!("{}[{}]", path, i)
1781                };
1782                validate_value(item, item_schema, &child_path, errors);
1783            }
1784        }
1785    }
1786}
1787
1788// =============================================================================
1789// Helper: extract JSON from text with possible markdown fences
1790// =============================================================================
1791
1792fn extract_json_from_text(text: &str) -> String {
1793    let trimmed = text.trim();
1794
1795    // Try to find ```json ... ``` or ``` ... ``` code fences
1796    if let Some(start) = trimmed.find("```") {
1797        let after_backticks = &trimmed[start + 3..];
1798        // Skip optional language tag (e.g., "json")
1799        let content_start = if let Some(nl) = after_backticks.find('\n') {
1800            nl + 1
1801        } else {
1802            0
1803        };
1804        let content = &after_backticks[content_start..];
1805        if let Some(end) = content.find("```") {
1806            return content[..end].trim().to_string();
1807        }
1808    }
1809
1810    // No code fences found; try to find JSON object or array boundaries
1811    // Look for first { or [ and last matching } or ]
1812    if let Some(obj_start) = trimmed.find('{') {
1813        if let Some(obj_end) = trimmed.rfind('}') {
1814            if obj_end > obj_start {
1815                return trimmed[obj_start..=obj_end].to_string();
1816            }
1817        }
1818    }
1819    if let Some(arr_start) = trimmed.find('[') {
1820        if let Some(arr_end) = trimmed.rfind(']') {
1821            if arr_end > arr_start {
1822                return trimmed[arr_start..=arr_end].to_string();
1823            }
1824        }
1825    }
1826
1827    // Fall back to trimmed text as-is
1828    trimmed.to_string()
1829}
1830
1831// =============================================================================
1832// Helper: convert process::Output to VmValue dict
1833// =============================================================================
1834
1835fn vm_output_to_value(output: std::process::Output) -> VmValue {
1836    let mut result = BTreeMap::new();
1837    result.insert(
1838        "stdout".to_string(),
1839        VmValue::String(Rc::from(
1840            String::from_utf8_lossy(&output.stdout).to_string().as_str(),
1841        )),
1842    );
1843    result.insert(
1844        "stderr".to_string(),
1845        VmValue::String(Rc::from(
1846            String::from_utf8_lossy(&output.stderr).to_string().as_str(),
1847        )),
1848    );
1849    result.insert(
1850        "status".to_string(),
1851        VmValue::Int(output.status.code().unwrap_or(-1) as i64),
1852    );
1853    result.insert(
1854        "success".to_string(),
1855        VmValue::Bool(output.status.success()),
1856    );
1857    VmValue::Dict(Rc::new(result))
1858}
1859
1860// =============================================================================
1861// Helper: civil date from timestamp (Howard Hinnant's algorithm)
1862// =============================================================================
1863
1864fn vm_civil_from_timestamp(total_secs: u64) -> (i64, i64, i64, i64, i64, i64, i64) {
1865    let days = total_secs / 86400;
1866    let time_of_day = total_secs % 86400;
1867    let hour = (time_of_day / 3600) as i64;
1868    let minute = ((time_of_day % 3600) / 60) as i64;
1869    let second = (time_of_day % 60) as i64;
1870
1871    let z = days as i64 + 719468;
1872    let era = if z >= 0 { z } else { z - 146096 } / 146097;
1873    let doe = (z - era * 146097) as u64;
1874    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
1875    let y = yoe as i64 + era * 400;
1876    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
1877    let mp = (5 * doy + 2) / 153;
1878    let d = (doy - (153 * mp + 2) / 5 + 1) as i64;
1879    let m = if mp < 10 { mp + 3 } else { mp - 9 } as i64;
1880    let y = if m <= 2 { y + 1 } else { y };
1881    let dow = ((days + 4) % 7) as i64;
1882
1883    (y, m, d, hour, minute, second, dow)
1884}
1885
1886// =============================================================================
1887// Logging helpers for VM
1888// =============================================================================
1889
1890pub(crate) static VM_MIN_LOG_LEVEL: AtomicU8 = AtomicU8::new(0);
1891
1892#[derive(Clone)]
1893pub(crate) struct VmTraceContext {
1894    pub(crate) trace_id: String,
1895    pub(crate) span_id: String,
1896}
1897
1898thread_local! {
1899    pub(crate) static VM_TRACE_STACK: std::cell::RefCell<Vec<VmTraceContext>> = const { std::cell::RefCell::new(Vec::new()) };
1900}
1901
1902fn vm_level_to_u8(level: &str) -> Option<u8> {
1903    match level {
1904        "debug" => Some(0),
1905        "info" => Some(1),
1906        "warn" => Some(2),
1907        "error" => Some(3),
1908        _ => None,
1909    }
1910}
1911
1912fn vm_format_timestamp_utc() -> String {
1913    let now = std::time::SystemTime::now()
1914        .duration_since(std::time::UNIX_EPOCH)
1915        .unwrap_or_default();
1916    let total_secs = now.as_secs();
1917    let millis = now.subsec_millis();
1918
1919    let days = total_secs / 86400;
1920    let time_of_day = total_secs % 86400;
1921    let hour = time_of_day / 3600;
1922    let minute = (time_of_day % 3600) / 60;
1923    let second = time_of_day % 60;
1924
1925    let z = days as i64 + 719468;
1926    let era = if z >= 0 { z } else { z - 146096 } / 146097;
1927    let doe = (z - era * 146097) as u64;
1928    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
1929    let y = yoe as i64 + era * 400;
1930    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
1931    let mp = (5 * doy + 2) / 153;
1932    let d = doy - (153 * mp + 2) / 5 + 1;
1933    let m = if mp < 10 { mp + 3 } else { mp - 9 };
1934    let y = if m <= 2 { y + 1 } else { y };
1935
1936    format!("{y:04}-{m:02}-{d:02}T{hour:02}:{minute:02}:{second:02}.{millis:03}Z")
1937}
1938
1939pub(crate) fn vm_escape_json_str(s: &str) -> String {
1940    let mut out = String::with_capacity(s.len());
1941    for ch in s.chars() {
1942        match ch {
1943            '"' => out.push_str("\\\""),
1944            '\\' => out.push_str("\\\\"),
1945            '\n' => out.push_str("\\n"),
1946            '\r' => out.push_str("\\r"),
1947            '\t' => out.push_str("\\t"),
1948            c if c.is_control() => {
1949                out.push_str(&format!("\\u{:04x}", c as u32));
1950            }
1951            c => out.push(c),
1952        }
1953    }
1954    out
1955}
1956
1957fn vm_escape_json_str_quoted(s: &str) -> String {
1958    let mut out = String::with_capacity(s.len() + 2);
1959    out.push('"');
1960    out.push_str(&vm_escape_json_str(s));
1961    out.push('"');
1962    out
1963}
1964
1965fn vm_value_to_json_fragment(val: &VmValue) -> String {
1966    match val {
1967        VmValue::String(s) => vm_escape_json_str_quoted(s),
1968        VmValue::Int(n) => n.to_string(),
1969        VmValue::Float(n) => {
1970            if n.is_finite() {
1971                n.to_string()
1972            } else {
1973                "null".to_string()
1974            }
1975        }
1976        VmValue::Bool(b) => b.to_string(),
1977        VmValue::Nil => "null".to_string(),
1978        _ => vm_escape_json_str_quoted(&val.display()),
1979    }
1980}
1981
1982fn vm_build_log_line(level: &str, msg: &str, fields: Option<&BTreeMap<String, VmValue>>) -> String {
1983    let ts = vm_format_timestamp_utc();
1984    let mut parts: Vec<String> = Vec::new();
1985    parts.push(format!("\"ts\":{}", vm_escape_json_str_quoted(&ts)));
1986    parts.push(format!("\"level\":{}", vm_escape_json_str_quoted(level)));
1987    parts.push(format!("\"msg\":{}", vm_escape_json_str_quoted(msg)));
1988
1989    VM_TRACE_STACK.with(|stack| {
1990        if let Some(trace) = stack.borrow().last() {
1991            parts.push(format!(
1992                "\"trace_id\":{}",
1993                vm_escape_json_str_quoted(&trace.trace_id)
1994            ));
1995            parts.push(format!(
1996                "\"span_id\":{}",
1997                vm_escape_json_str_quoted(&trace.span_id)
1998            ));
1999        }
2000    });
2001
2002    if let Some(dict) = fields {
2003        for (k, v) in dict {
2004            parts.push(format!(
2005                "{}:{}",
2006                vm_escape_json_str_quoted(k),
2007                vm_value_to_json_fragment(v)
2008            ));
2009        }
2010    }
2011
2012    format!("{{{}}}\n", parts.join(","))
2013}
2014
2015fn vm_write_log(level: &str, level_num: u8, args: &[VmValue], out: &mut String) {
2016    if level_num < VM_MIN_LOG_LEVEL.load(Ordering::Relaxed) {
2017        return;
2018    }
2019    let msg = args.first().map(|a| a.display()).unwrap_or_default();
2020    let fields = args.get(1).and_then(|v| {
2021        if let VmValue::Dict(d) = v {
2022            Some(&**d)
2023        } else {
2024            None
2025        }
2026    });
2027    let line = vm_build_log_line(level, &msg, fields);
2028    out.push_str(&line);
2029}
2030
2031// =============================================================================
2032// Tool registry helpers for VM
2033// =============================================================================
2034
2035fn vm_validate_registry(name: &str, dict: &BTreeMap<String, VmValue>) -> Result<(), VmError> {
2036    match dict.get("_type") {
2037        Some(VmValue::String(t)) if &**t == "tool_registry" => Ok(()),
2038        _ => Err(VmError::Thrown(VmValue::String(Rc::from(format!(
2039            "{name}: argument must be a tool registry (created with tool_registry())"
2040        ))))),
2041    }
2042}
2043
2044fn vm_get_tools(dict: &BTreeMap<String, VmValue>) -> &[VmValue] {
2045    match dict.get("tools") {
2046        Some(VmValue::List(list)) => list,
2047        _ => &[],
2048    }
2049}
2050
2051fn vm_format_parameters(params: Option<&VmValue>) -> String {
2052    match params {
2053        Some(VmValue::Dict(map)) if !map.is_empty() => {
2054            let mut pairs: Vec<(String, String)> =
2055                map.iter().map(|(k, v)| (k.clone(), v.display())).collect();
2056            pairs.sort_by(|a, b| a.0.cmp(&b.0));
2057            pairs
2058                .iter()
2059                .map(|(k, v)| format!("{k}: {v}"))
2060                .collect::<Vec<_>>()
2061                .join(", ")
2062        }
2063        _ => String::new(),
2064    }
2065}
2066
2067fn vm_build_empty_schema() -> BTreeMap<String, VmValue> {
2068    let mut schema = BTreeMap::new();
2069    schema.insert(
2070        "schema_version".to_string(),
2071        VmValue::String(Rc::from("harn-tools/1.0")),
2072    );
2073    schema.insert("tools".to_string(), VmValue::List(Rc::new(Vec::new())));
2074    schema
2075}
2076
2077fn vm_build_input_schema(
2078    params: Option<&VmValue>,
2079    components: Option<&BTreeMap<String, VmValue>>,
2080) -> VmValue {
2081    let mut schema = BTreeMap::new();
2082    schema.insert("type".to_string(), VmValue::String(Rc::from("object")));
2083
2084    let params_map = match params {
2085        Some(VmValue::Dict(map)) if !map.is_empty() => map,
2086        _ => {
2087            schema.insert(
2088                "properties".to_string(),
2089                VmValue::Dict(Rc::new(BTreeMap::new())),
2090            );
2091            return VmValue::Dict(Rc::new(schema));
2092        }
2093    };
2094
2095    let mut properties = BTreeMap::new();
2096    let mut required = Vec::new();
2097
2098    for (key, val) in params_map.iter() {
2099        let prop = vm_resolve_param_type(val, components);
2100        properties.insert(key.clone(), prop);
2101        required.push(VmValue::String(Rc::from(key.as_str())));
2102    }
2103
2104    schema.insert("properties".to_string(), VmValue::Dict(Rc::new(properties)));
2105    if !required.is_empty() {
2106        required.sort_by_key(|a| a.display());
2107        schema.insert("required".to_string(), VmValue::List(Rc::new(required)));
2108    }
2109
2110    VmValue::Dict(Rc::new(schema))
2111}
2112
2113fn vm_resolve_param_type(val: &VmValue, components: Option<&BTreeMap<String, VmValue>>) -> VmValue {
2114    match val {
2115        VmValue::String(type_name) => {
2116            let json_type = vm_harn_type_to_json_schema(type_name);
2117            let mut prop = BTreeMap::new();
2118            prop.insert("type".to_string(), VmValue::String(Rc::from(json_type)));
2119            VmValue::Dict(Rc::new(prop))
2120        }
2121        VmValue::Dict(map) => {
2122            if let Some(VmValue::String(ref_name)) = map.get("$ref") {
2123                if let Some(comps) = components {
2124                    if let Some(resolved) = comps.get(&**ref_name) {
2125                        return resolved.clone();
2126                    }
2127                }
2128                let mut prop = BTreeMap::new();
2129                prop.insert(
2130                    "$ref".to_string(),
2131                    VmValue::String(Rc::from(
2132                        format!("#/components/schemas/{ref_name}").as_str(),
2133                    )),
2134                );
2135                VmValue::Dict(Rc::new(prop))
2136            } else {
2137                VmValue::Dict(Rc::new((**map).clone()))
2138            }
2139        }
2140        _ => {
2141            let mut prop = BTreeMap::new();
2142            prop.insert("type".to_string(), VmValue::String(Rc::from("string")));
2143            VmValue::Dict(Rc::new(prop))
2144        }
2145    }
2146}
2147
2148fn vm_harn_type_to_json_schema(harn_type: &str) -> &str {
2149    match harn_type {
2150        "int" => "integer",
2151        "float" => "number",
2152        "bool" | "boolean" => "boolean",
2153        "list" | "array" => "array",
2154        "dict" | "object" => "object",
2155        _ => "string",
2156    }
2157}