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    // Logging builtins
765    // =========================================================================
766
767    vm.register_builtin("log_debug", |args, out| {
768        vm_write_log("debug", 0, args, out);
769        Ok(VmValue::Nil)
770    });
771
772    vm.register_builtin("log_info", |args, out| {
773        vm_write_log("info", 1, args, out);
774        Ok(VmValue::Nil)
775    });
776
777    vm.register_builtin("log_warn", |args, out| {
778        vm_write_log("warn", 2, args, out);
779        Ok(VmValue::Nil)
780    });
781
782    vm.register_builtin("log_error", |args, out| {
783        vm_write_log("error", 3, args, out);
784        Ok(VmValue::Nil)
785    });
786
787    vm.register_builtin("log_set_level", |args, _out| {
788        let level_str = args.first().map(|a| a.display()).unwrap_or_default();
789        match vm_level_to_u8(&level_str) {
790            Some(n) => {
791                VM_MIN_LOG_LEVEL.store(n, Ordering::Relaxed);
792                Ok(VmValue::Nil)
793            }
794            None => Err(VmError::Thrown(VmValue::String(Rc::from(format!(
795                "log_set_level: invalid level '{}'. Expected debug, info, warn, or error",
796                level_str
797            ))))),
798        }
799    });
800
801    // =========================================================================
802    // Tracing builtins
803    // =========================================================================
804
805    vm.register_builtin("trace_start", |args, _out| {
806        use rand::Rng;
807        let name = args.first().map(|a| a.display()).unwrap_or_default();
808        let trace_id = VM_TRACE_STACK.with(|stack| {
809            stack
810                .borrow()
811                .last()
812                .map(|t| t.trace_id.clone())
813                .unwrap_or_else(|| {
814                    let val: u32 = rand::thread_rng().gen();
815                    format!("{val:08x}")
816                })
817        });
818        let span_id = {
819            let val: u32 = rand::thread_rng().gen();
820            format!("{val:08x}")
821        };
822        let start_ms = std::time::SystemTime::now()
823            .duration_since(std::time::UNIX_EPOCH)
824            .unwrap_or_default()
825            .as_millis() as i64;
826
827        VM_TRACE_STACK.with(|stack| {
828            stack.borrow_mut().push(VmTraceContext {
829                trace_id: trace_id.clone(),
830                span_id: span_id.clone(),
831            });
832        });
833
834        let mut span = BTreeMap::new();
835        span.insert(
836            "trace_id".to_string(),
837            VmValue::String(Rc::from(trace_id.as_str())),
838        );
839        span.insert(
840            "span_id".to_string(),
841            VmValue::String(Rc::from(span_id.as_str())),
842        );
843        span.insert("name".to_string(), VmValue::String(Rc::from(name.as_str())));
844        span.insert("start_ms".to_string(), VmValue::Int(start_ms));
845        Ok(VmValue::Dict(Rc::new(span)))
846    });
847
848    vm.register_builtin("trace_end", |args, out| {
849        let span = match args.first() {
850            Some(VmValue::Dict(d)) => d,
851            _ => {
852                return Err(VmError::Thrown(VmValue::String(Rc::from(
853                    "trace_end: argument must be a span dict from trace_start",
854                ))));
855            }
856        };
857
858        let end_ms = std::time::SystemTime::now()
859            .duration_since(std::time::UNIX_EPOCH)
860            .unwrap_or_default()
861            .as_millis() as i64;
862
863        let start_ms = span
864            .get("start_ms")
865            .and_then(|v| v.as_int())
866            .unwrap_or(end_ms);
867        let duration_ms = end_ms - start_ms;
868        let name = span.get("name").map(|v| v.display()).unwrap_or_default();
869        let trace_id = span
870            .get("trace_id")
871            .map(|v| v.display())
872            .unwrap_or_default();
873        let span_id = span.get("span_id").map(|v| v.display()).unwrap_or_default();
874
875        VM_TRACE_STACK.with(|stack| {
876            stack.borrow_mut().pop();
877        });
878
879        let level_num = 1_u8;
880        if level_num >= VM_MIN_LOG_LEVEL.load(Ordering::Relaxed) {
881            let mut fields = BTreeMap::new();
882            fields.insert(
883                "trace_id".to_string(),
884                VmValue::String(Rc::from(trace_id.as_str())),
885            );
886            fields.insert(
887                "span_id".to_string(),
888                VmValue::String(Rc::from(span_id.as_str())),
889            );
890            fields.insert("name".to_string(), VmValue::String(Rc::from(name.as_str())));
891            fields.insert("duration_ms".to_string(), VmValue::Int(duration_ms));
892            let line = vm_build_log_line("info", "span_end", Some(&fields));
893            out.push_str(&line);
894        }
895
896        Ok(VmValue::Nil)
897    });
898
899    vm.register_builtin("trace_id", |_args, _out| {
900        let id = VM_TRACE_STACK.with(|stack| stack.borrow().last().map(|t| t.trace_id.clone()));
901        match id {
902            Some(trace_id) => Ok(VmValue::String(Rc::from(trace_id.as_str()))),
903            None => Ok(VmValue::Nil),
904        }
905    });
906
907    // =========================================================================
908    // Tool registry builtins
909    // =========================================================================
910
911    vm.register_builtin("tool_registry", |_args, _out| {
912        let mut registry = BTreeMap::new();
913        registry.insert(
914            "_type".to_string(),
915            VmValue::String(Rc::from("tool_registry")),
916        );
917        registry.insert("tools".to_string(), VmValue::List(Rc::new(Vec::new())));
918        Ok(VmValue::Dict(Rc::new(registry)))
919    });
920
921    vm.register_builtin("tool_add", |args, _out| {
922        if args.len() < 4 {
923            return Err(VmError::Thrown(VmValue::String(Rc::from(
924                "tool_add: requires registry, name, description, and handler",
925            ))));
926        }
927
928        let registry = match &args[0] {
929            VmValue::Dict(map) => (**map).clone(),
930            _ => {
931                return Err(VmError::Thrown(VmValue::String(Rc::from(
932                    "tool_add: first argument must be a tool registry",
933                ))));
934            }
935        };
936
937        match registry.get("_type") {
938            Some(VmValue::String(t)) if &**t == "tool_registry" => {}
939            _ => {
940                return Err(VmError::Thrown(VmValue::String(Rc::from(
941                    "tool_add: first argument must be a tool registry",
942                ))));
943            }
944        }
945
946        let name = args[1].display();
947        let description = args[2].display();
948        let handler = args[3].clone();
949        let parameters = if args.len() > 4 {
950            args[4].clone()
951        } else {
952            VmValue::Dict(Rc::new(BTreeMap::new()))
953        };
954
955        let mut tool_entry = BTreeMap::new();
956        tool_entry.insert("name".to_string(), VmValue::String(Rc::from(name.as_str())));
957        tool_entry.insert(
958            "description".to_string(),
959            VmValue::String(Rc::from(description.as_str())),
960        );
961        tool_entry.insert("handler".to_string(), handler);
962        tool_entry.insert("parameters".to_string(), parameters);
963
964        let mut tools: Vec<VmValue> = match registry.get("tools") {
965            Some(VmValue::List(list)) => list
966                .iter()
967                .filter(|t| {
968                    if let VmValue::Dict(e) = t {
969                        e.get("name").map(|v| v.display()).as_deref() != Some(name.as_str())
970                    } else {
971                        true
972                    }
973                })
974                .cloned()
975                .collect(),
976            _ => Vec::new(),
977        };
978        tools.push(VmValue::Dict(Rc::new(tool_entry)));
979
980        let mut new_registry = registry;
981        new_registry.insert("tools".to_string(), VmValue::List(Rc::new(tools)));
982        Ok(VmValue::Dict(Rc::new(new_registry)))
983    });
984
985    vm.register_builtin("tool_list", |args, _out| {
986        let registry = match args.first() {
987            Some(VmValue::Dict(map)) => map,
988            _ => {
989                return Err(VmError::Thrown(VmValue::String(Rc::from(
990                    "tool_list: requires a tool registry",
991                ))));
992            }
993        };
994        vm_validate_registry("tool_list", registry)?;
995
996        let tools = vm_get_tools(registry);
997        let mut result = Vec::new();
998        for tool in tools {
999            if let VmValue::Dict(entry) = tool {
1000                let mut desc = BTreeMap::new();
1001                if let Some(name) = entry.get("name") {
1002                    desc.insert("name".to_string(), name.clone());
1003                }
1004                if let Some(description) = entry.get("description") {
1005                    desc.insert("description".to_string(), description.clone());
1006                }
1007                if let Some(parameters) = entry.get("parameters") {
1008                    desc.insert("parameters".to_string(), parameters.clone());
1009                }
1010                result.push(VmValue::Dict(Rc::new(desc)));
1011            }
1012        }
1013        Ok(VmValue::List(Rc::new(result)))
1014    });
1015
1016    vm.register_builtin("tool_find", |args, _out| {
1017        if args.len() < 2 {
1018            return Err(VmError::Thrown(VmValue::String(Rc::from(
1019                "tool_find: requires registry and name",
1020            ))));
1021        }
1022
1023        let registry = match &args[0] {
1024            VmValue::Dict(map) => map,
1025            _ => {
1026                return Err(VmError::Thrown(VmValue::String(Rc::from(
1027                    "tool_find: first argument must be a tool registry",
1028                ))));
1029            }
1030        };
1031        vm_validate_registry("tool_find", registry)?;
1032
1033        let target_name = args[1].display();
1034        let tools = vm_get_tools(registry);
1035
1036        for tool in tools {
1037            if let VmValue::Dict(entry) = tool {
1038                if let Some(VmValue::String(name)) = entry.get("name") {
1039                    if &**name == target_name.as_str() {
1040                        return Ok(tool.clone());
1041                    }
1042                }
1043            }
1044        }
1045        Ok(VmValue::Nil)
1046    });
1047
1048    vm.register_builtin("tool_describe", |args, _out| {
1049        let registry = match args.first() {
1050            Some(VmValue::Dict(map)) => map,
1051            _ => {
1052                return Err(VmError::Thrown(VmValue::String(Rc::from(
1053                    "tool_describe: requires a tool registry",
1054                ))));
1055            }
1056        };
1057        vm_validate_registry("tool_describe", registry)?;
1058
1059        let tools = vm_get_tools(registry);
1060
1061        if tools.is_empty() {
1062            return Ok(VmValue::String(Rc::from("Available tools:\n(none)")));
1063        }
1064
1065        let mut tool_infos: Vec<(String, String, String)> = Vec::new();
1066        for tool in tools {
1067            if let VmValue::Dict(entry) = tool {
1068                let name = entry.get("name").map(|v| v.display()).unwrap_or_default();
1069                let description = entry
1070                    .get("description")
1071                    .map(|v| v.display())
1072                    .unwrap_or_default();
1073                let params_str = vm_format_parameters(entry.get("parameters"));
1074                tool_infos.push((name, params_str, description));
1075            }
1076        }
1077
1078        tool_infos.sort_by(|a, b| a.0.cmp(&b.0));
1079
1080        let mut lines = vec!["Available tools:".to_string()];
1081        for (name, params, desc) in &tool_infos {
1082            lines.push(format!("- {name}({params}): {desc}"));
1083        }
1084
1085        Ok(VmValue::String(Rc::from(lines.join("\n").as_str())))
1086    });
1087
1088    vm.register_builtin("tool_remove", |args, _out| {
1089        if args.len() < 2 {
1090            return Err(VmError::Thrown(VmValue::String(Rc::from(
1091                "tool_remove: requires registry and name",
1092            ))));
1093        }
1094
1095        let registry = match &args[0] {
1096            VmValue::Dict(map) => (**map).clone(),
1097            _ => {
1098                return Err(VmError::Thrown(VmValue::String(Rc::from(
1099                    "tool_remove: first argument must be a tool registry",
1100                ))));
1101            }
1102        };
1103        vm_validate_registry("tool_remove", &registry)?;
1104
1105        let target_name = args[1].display();
1106
1107        let tools = match registry.get("tools") {
1108            Some(VmValue::List(list)) => (**list).clone(),
1109            _ => Vec::new(),
1110        };
1111
1112        let filtered: Vec<VmValue> = tools
1113            .into_iter()
1114            .filter(|tool| {
1115                if let VmValue::Dict(entry) = tool {
1116                    if let Some(VmValue::String(name)) = entry.get("name") {
1117                        return &**name != target_name.as_str();
1118                    }
1119                }
1120                true
1121            })
1122            .collect();
1123
1124        let mut new_registry = registry;
1125        new_registry.insert("tools".to_string(), VmValue::List(Rc::new(filtered)));
1126        Ok(VmValue::Dict(Rc::new(new_registry)))
1127    });
1128
1129    vm.register_builtin("tool_count", |args, _out| {
1130        let registry = match args.first() {
1131            Some(VmValue::Dict(map)) => map,
1132            _ => {
1133                return Err(VmError::Thrown(VmValue::String(Rc::from(
1134                    "tool_count: requires a tool registry",
1135                ))));
1136            }
1137        };
1138        vm_validate_registry("tool_count", registry)?;
1139        let count = vm_get_tools(registry).len();
1140        Ok(VmValue::Int(count as i64))
1141    });
1142
1143    vm.register_builtin("tool_schema", |args, _out| {
1144        let registry = match args.first() {
1145            Some(VmValue::Dict(map)) => {
1146                vm_validate_registry("tool_schema", map)?;
1147                map
1148            }
1149            _ => {
1150                return Err(VmError::Thrown(VmValue::String(Rc::from(
1151                    "tool_schema: requires a tool registry",
1152                ))));
1153            }
1154        };
1155
1156        let components = args.get(1).and_then(|v| v.as_dict()).cloned();
1157
1158        let tools = match registry.get("tools") {
1159            Some(VmValue::List(list)) => list,
1160            _ => return Ok(VmValue::Dict(Rc::new(vm_build_empty_schema()))),
1161        };
1162
1163        let mut tool_schemas = Vec::new();
1164        for tool in tools.iter() {
1165            if let VmValue::Dict(entry) = tool {
1166                let name = entry.get("name").map(|v| v.display()).unwrap_or_default();
1167                let description = entry
1168                    .get("description")
1169                    .map(|v| v.display())
1170                    .unwrap_or_default();
1171
1172                let input_schema =
1173                    vm_build_input_schema(entry.get("parameters"), components.as_ref());
1174
1175                let mut tool_def = BTreeMap::new();
1176                tool_def.insert("name".to_string(), VmValue::String(Rc::from(name.as_str())));
1177                tool_def.insert(
1178                    "description".to_string(),
1179                    VmValue::String(Rc::from(description.as_str())),
1180                );
1181                tool_def.insert("inputSchema".to_string(), input_schema);
1182                tool_schemas.push(VmValue::Dict(Rc::new(tool_def)));
1183            }
1184        }
1185
1186        let mut schema = BTreeMap::new();
1187        schema.insert(
1188            "schema_version".to_string(),
1189            VmValue::String(Rc::from("harn-tools/1.0")),
1190        );
1191
1192        if let Some(comps) = &components {
1193            let mut comp_wrapper = BTreeMap::new();
1194            comp_wrapper.insert("schemas".to_string(), VmValue::Dict(Rc::new(comps.clone())));
1195            schema.insert(
1196                "components".to_string(),
1197                VmValue::Dict(Rc::new(comp_wrapper)),
1198            );
1199        }
1200
1201        schema.insert("tools".to_string(), VmValue::List(Rc::new(tool_schemas)));
1202        Ok(VmValue::Dict(Rc::new(schema)))
1203    });
1204
1205    vm.register_builtin("tool_parse_call", |args, _out| {
1206        let text = args.first().map(|a| a.display()).unwrap_or_default();
1207
1208        let mut results = Vec::new();
1209        let mut search_from = 0;
1210
1211        while let Some(start) = text[search_from..].find("<tool_call>") {
1212            let abs_start = search_from + start + "<tool_call>".len();
1213            if let Some(end) = text[abs_start..].find("</tool_call>") {
1214                let json_str = text[abs_start..abs_start + end].trim();
1215                if let Ok(jv) = serde_json::from_str::<serde_json::Value>(json_str) {
1216                    results.push(json_to_vm_value(&jv));
1217                }
1218                search_from = abs_start + end + "</tool_call>".len();
1219            } else {
1220                break;
1221            }
1222        }
1223
1224        Ok(VmValue::List(Rc::new(results)))
1225    });
1226
1227    vm.register_builtin("tool_format_result", |args, _out| {
1228        if args.len() < 2 {
1229            return Err(VmError::Thrown(VmValue::String(Rc::from(
1230                "tool_format_result: requires name and result",
1231            ))));
1232        }
1233        let name = args[0].display();
1234        let result = args[1].display();
1235
1236        let json_name = vm_escape_json_str(&name);
1237        let json_result = vm_escape_json_str(&result);
1238        Ok(VmValue::String(Rc::from(
1239            format!(
1240                "<tool_result>{{\"name\": \"{json_name}\", \"result\": \"{json_result}\"}}</tool_result>"
1241            )
1242            .as_str(),
1243        )))
1244    });
1245
1246    vm.register_builtin("tool_prompt", |args, _out| {
1247        let registry = match args.first() {
1248            Some(VmValue::Dict(map)) => {
1249                vm_validate_registry("tool_prompt", map)?;
1250                map
1251            }
1252            _ => {
1253                return Err(VmError::Thrown(VmValue::String(Rc::from(
1254                    "tool_prompt: requires a tool registry",
1255                ))));
1256            }
1257        };
1258
1259        let tools = match registry.get("tools") {
1260            Some(VmValue::List(list)) => list,
1261            _ => {
1262                return Ok(VmValue::String(Rc::from("No tools are available.")));
1263            }
1264        };
1265
1266        if tools.is_empty() {
1267            return Ok(VmValue::String(Rc::from("No tools are available.")));
1268        }
1269
1270        let mut prompt = String::from("# Available Tools\n\n");
1271        prompt.push_str("You have access to the following tools. To use a tool, output a tool call in this exact format:\n\n");
1272        prompt.push_str("<tool_call>{\"name\": \"tool_name\", \"arguments\": {\"param\": \"value\"}}</tool_call>\n\n");
1273        prompt.push_str("You may make multiple tool calls in a single response. Wait for tool results before proceeding.\n\n");
1274        prompt.push_str("## Tools\n\n");
1275
1276        let mut tool_infos: Vec<(&BTreeMap<String, VmValue>, String)> = Vec::new();
1277        for tool in tools.iter() {
1278            if let VmValue::Dict(entry) = tool {
1279                let name = entry.get("name").map(|v| v.display()).unwrap_or_default();
1280                tool_infos.push((entry, name));
1281            }
1282        }
1283        tool_infos.sort_by(|a, b| a.1.cmp(&b.1));
1284
1285        for (entry, name) in &tool_infos {
1286            let description = entry
1287                .get("description")
1288                .map(|v| v.display())
1289                .unwrap_or_default();
1290            let params_str = vm_format_parameters(entry.get("parameters"));
1291
1292            prompt.push_str(&format!("### {name}\n"));
1293            prompt.push_str(&format!("{description}\n"));
1294            if !params_str.is_empty() {
1295                prompt.push_str(&format!("Parameters: {params_str}\n"));
1296            }
1297            prompt.push('\n');
1298        }
1299
1300        Ok(VmValue::String(Rc::from(prompt.trim_end())))
1301    });
1302
1303    // =========================================================================
1304    // Channel builtins (sync)
1305    // =========================================================================
1306
1307    vm.register_builtin("channel", |args, _out| {
1308        let name = args
1309            .first()
1310            .map(|a| a.display())
1311            .unwrap_or_else(|| "default".to_string());
1312        let capacity = args.get(1).and_then(|a| a.as_int()).unwrap_or(256) as usize;
1313        let capacity = capacity.max(1);
1314        let (tx, rx) = tokio::sync::mpsc::channel(capacity);
1315        #[allow(clippy::arc_with_non_send_sync)]
1316        Ok(VmValue::Channel(VmChannelHandle {
1317            name,
1318            sender: Arc::new(tx),
1319            receiver: Arc::new(tokio::sync::Mutex::new(rx)),
1320            closed: Arc::new(AtomicBool::new(false)),
1321        }))
1322    });
1323
1324    vm.register_builtin("close_channel", |args, _out| {
1325        if args.is_empty() {
1326            return Err(VmError::Thrown(VmValue::String(Rc::from(
1327                "close_channel: requires a channel",
1328            ))));
1329        }
1330        if let VmValue::Channel(ch) = &args[0] {
1331            ch.closed.store(true, Ordering::SeqCst);
1332            Ok(VmValue::Nil)
1333        } else {
1334            Err(VmError::Thrown(VmValue::String(Rc::from(
1335                "close_channel: first argument must be a channel",
1336            ))))
1337        }
1338    });
1339
1340    vm.register_builtin("try_receive", |args, _out| {
1341        if args.is_empty() {
1342            return Err(VmError::Thrown(VmValue::String(Rc::from(
1343                "try_receive: requires a channel",
1344            ))));
1345        }
1346        if let VmValue::Channel(ch) = &args[0] {
1347            match ch.receiver.try_lock() {
1348                Ok(mut rx) => match rx.try_recv() {
1349                    Ok(val) => Ok(val),
1350                    Err(_) => Ok(VmValue::Nil),
1351                },
1352                Err(_) => Ok(VmValue::Nil),
1353            }
1354        } else {
1355            Err(VmError::Thrown(VmValue::String(Rc::from(
1356                "try_receive: first argument must be a channel",
1357            ))))
1358        }
1359    });
1360
1361    // =========================================================================
1362    // Atomic builtins
1363    // =========================================================================
1364
1365    vm.register_builtin("atomic", |args, _out| {
1366        let initial = match args.first() {
1367            Some(VmValue::Int(n)) => *n,
1368            Some(VmValue::Float(f)) => *f as i64,
1369            Some(VmValue::Bool(b)) => {
1370                if *b {
1371                    1
1372                } else {
1373                    0
1374                }
1375            }
1376            _ => 0,
1377        };
1378        Ok(VmValue::Atomic(VmAtomicHandle {
1379            value: Arc::new(AtomicI64::new(initial)),
1380        }))
1381    });
1382
1383    vm.register_builtin("atomic_get", |args, _out| {
1384        if let Some(VmValue::Atomic(a)) = args.first() {
1385            Ok(VmValue::Int(a.value.load(Ordering::SeqCst)))
1386        } else {
1387            Ok(VmValue::Nil)
1388        }
1389    });
1390
1391    vm.register_builtin("atomic_set", |args, _out| {
1392        if args.len() >= 2 {
1393            if let (VmValue::Atomic(a), Some(val)) = (&args[0], args[1].as_int()) {
1394                let old = a.value.swap(val, Ordering::SeqCst);
1395                return Ok(VmValue::Int(old));
1396            }
1397        }
1398        Ok(VmValue::Nil)
1399    });
1400
1401    vm.register_builtin("atomic_add", |args, _out| {
1402        if args.len() >= 2 {
1403            if let (VmValue::Atomic(a), Some(delta)) = (&args[0], args[1].as_int()) {
1404                let prev = a.value.fetch_add(delta, Ordering::SeqCst);
1405                return Ok(VmValue::Int(prev));
1406            }
1407        }
1408        Ok(VmValue::Nil)
1409    });
1410
1411    vm.register_builtin("atomic_cas", |args, _out| {
1412        if args.len() >= 3 {
1413            if let (VmValue::Atomic(a), Some(expected), Some(new_val)) =
1414                (&args[0], args[1].as_int(), args[2].as_int())
1415            {
1416                let result =
1417                    a.value
1418                        .compare_exchange(expected, new_val, Ordering::SeqCst, Ordering::SeqCst);
1419                return Ok(VmValue::Bool(result.is_ok()));
1420            }
1421        }
1422        Ok(VmValue::Bool(false))
1423    });
1424
1425    // =========================================================================
1426    // Async builtins
1427    // =========================================================================
1428
1429    // sleep(ms)
1430    vm.register_async_builtin("sleep", |args| async move {
1431        let ms = match args.first() {
1432            Some(VmValue::Duration(ms)) => *ms,
1433            Some(VmValue::Int(n)) => *n as u64,
1434            _ => 0,
1435        };
1436        if ms > 0 {
1437            tokio::time::sleep(tokio::time::Duration::from_millis(ms)).await;
1438        }
1439        Ok(VmValue::Nil)
1440    });
1441
1442    // send(channel, value)
1443    vm.register_async_builtin("send", |args| async move {
1444        if args.len() < 2 {
1445            return Err(VmError::Thrown(VmValue::String(Rc::from(
1446                "send: requires channel and value",
1447            ))));
1448        }
1449        if let VmValue::Channel(ch) = &args[0] {
1450            if ch.closed.load(Ordering::SeqCst) {
1451                return Ok(VmValue::Bool(false));
1452            }
1453            let val = args[1].clone();
1454            match ch.sender.send(val).await {
1455                Ok(()) => Ok(VmValue::Bool(true)),
1456                Err(_) => Ok(VmValue::Bool(false)),
1457            }
1458        } else {
1459            Err(VmError::Thrown(VmValue::String(Rc::from(
1460                "send: first argument must be a channel",
1461            ))))
1462        }
1463    });
1464
1465    // receive(channel)
1466    vm.register_async_builtin("receive", |args| async move {
1467        if args.is_empty() {
1468            return Err(VmError::Thrown(VmValue::String(Rc::from(
1469                "receive: requires a channel",
1470            ))));
1471        }
1472        if let VmValue::Channel(ch) = &args[0] {
1473            if ch.closed.load(Ordering::SeqCst) {
1474                let mut rx = ch.receiver.lock().await;
1475                return match rx.try_recv() {
1476                    Ok(val) => Ok(val),
1477                    Err(_) => Ok(VmValue::Nil),
1478                };
1479            }
1480            let mut rx = ch.receiver.lock().await;
1481            match rx.recv().await {
1482                Some(val) => Ok(val),
1483                None => Ok(VmValue::Nil),
1484            }
1485        } else {
1486            Err(VmError::Thrown(VmValue::String(Rc::from(
1487                "receive: first argument must be a channel",
1488            ))))
1489        }
1490    });
1491
1492    // select(channel1, channel2, ...)
1493    vm.register_async_builtin("select", |args| async move {
1494        if args.is_empty() {
1495            return Err(VmError::Thrown(VmValue::String(Rc::from(
1496                "select: requires at least one channel",
1497            ))));
1498        }
1499
1500        let mut channels: Vec<&VmChannelHandle> = Vec::new();
1501        for arg in &args {
1502            if let VmValue::Channel(ch) = arg {
1503                channels.push(ch);
1504            } else {
1505                return Err(VmError::Thrown(VmValue::String(Rc::from(
1506                    "select: all arguments must be channels",
1507                ))));
1508            }
1509        }
1510
1511        loop {
1512            let mut all_closed = true;
1513            for (i, ch) in channels.iter().enumerate() {
1514                if let Ok(mut rx) = ch.receiver.try_lock() {
1515                    match rx.try_recv() {
1516                        Ok(val) => {
1517                            let mut result = BTreeMap::new();
1518                            result.insert("index".to_string(), VmValue::Int(i as i64));
1519                            result.insert("value".to_string(), val);
1520                            result.insert(
1521                                "channel".to_string(),
1522                                VmValue::String(Rc::from(ch.name.as_str())),
1523                            );
1524                            return Ok(VmValue::Dict(Rc::new(result)));
1525                        }
1526                        Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {
1527                            all_closed = false;
1528                        }
1529                        Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {}
1530                    }
1531                }
1532            }
1533            if all_closed {
1534                return Ok(VmValue::Nil);
1535            }
1536            tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
1537        }
1538    });
1539
1540    // =========================================================================
1541    // JSON validation and extraction builtins
1542    // =========================================================================
1543
1544    vm.register_builtin("json_validate", |args, _out| {
1545        if args.len() < 2 {
1546            return Err(VmError::Thrown(VmValue::String(Rc::from(
1547                "json_validate requires 2 arguments: data and schema",
1548            ))));
1549        }
1550        let data = &args[0];
1551        let schema = &args[1];
1552        let schema_dict = match schema.as_dict() {
1553            Some(d) => d,
1554            None => {
1555                return Err(VmError::Thrown(VmValue::String(Rc::from(
1556                    "json_validate: schema must be a dict",
1557                ))));
1558            }
1559        };
1560        let mut errors = Vec::new();
1561        validate_value(data, schema_dict, "", &mut errors);
1562        if errors.is_empty() {
1563            Ok(VmValue::Bool(true))
1564        } else {
1565            Err(VmError::Thrown(VmValue::String(Rc::from(
1566                errors.join("; "),
1567            ))))
1568        }
1569    });
1570
1571    vm.register_builtin("json_extract", |args, _out| {
1572        if args.is_empty() {
1573            return Err(VmError::Thrown(VmValue::String(Rc::from(
1574                "json_extract requires at least 1 argument: text",
1575            ))));
1576        }
1577        let text = args[0].display();
1578        let key = args.get(1).map(|a| a.display());
1579
1580        // Extract JSON from text that may contain markdown code fences
1581        let json_str = extract_json_from_text(&text);
1582        let parsed = match serde_json::from_str::<serde_json::Value>(&json_str) {
1583            Ok(jv) => json_to_vm_value(&jv),
1584            Err(e) => {
1585                return Err(VmError::Thrown(VmValue::String(Rc::from(format!(
1586                    "json_extract: failed to parse JSON: {e}"
1587                )))));
1588            }
1589        };
1590
1591        match key {
1592            Some(k) => match &parsed {
1593                VmValue::Dict(map) => match map.get(&k) {
1594                    Some(val) => Ok(val.clone()),
1595                    None => Err(VmError::Thrown(VmValue::String(Rc::from(format!(
1596                        "json_extract: key '{}' not found",
1597                        k
1598                    ))))),
1599                },
1600                _ => Err(VmError::Thrown(VmValue::String(Rc::from(
1601                    "json_extract: parsed value is not a dict, cannot extract key",
1602                )))),
1603            },
1604            None => Ok(parsed),
1605        }
1606    });
1607
1608    // =========================================================================
1609    // HTTP and LLM builtins (registered from separate modules)
1610    // =========================================================================
1611
1612    register_http_builtins(vm);
1613    register_llm_builtins(vm);
1614    register_mcp_builtins(vm);
1615}
1616
1617// =============================================================================
1618// JSON helpers
1619// =============================================================================
1620
1621pub(crate) fn escape_json_string_vm(s: &str) -> String {
1622    let mut out = String::with_capacity(s.len() + 2);
1623    out.push('"');
1624    for ch in s.chars() {
1625        match ch {
1626            '"' => out.push_str("\\\""),
1627            '\\' => out.push_str("\\\\"),
1628            '\n' => out.push_str("\\n"),
1629            '\r' => out.push_str("\\r"),
1630            '\t' => out.push_str("\\t"),
1631            c if c.is_control() => {
1632                out.push_str(&format!("\\u{:04x}", c as u32));
1633            }
1634            c => out.push(c),
1635        }
1636    }
1637    out.push('"');
1638    out
1639}
1640
1641pub(crate) fn vm_value_to_json(val: &VmValue) -> String {
1642    match val {
1643        VmValue::String(s) => escape_json_string_vm(s),
1644        VmValue::Int(n) => n.to_string(),
1645        VmValue::Float(n) => n.to_string(),
1646        VmValue::Bool(b) => b.to_string(),
1647        VmValue::Nil => "null".to_string(),
1648        VmValue::List(items) => {
1649            let inner: Vec<String> = items.iter().map(vm_value_to_json).collect();
1650            format!("[{}]", inner.join(","))
1651        }
1652        VmValue::Dict(map) => {
1653            let inner: Vec<String> = map
1654                .iter()
1655                .map(|(k, v)| format!("{}:{}", escape_json_string_vm(k), vm_value_to_json(v)))
1656                .collect();
1657            format!("{{{}}}", inner.join(","))
1658        }
1659        _ => "null".to_string(),
1660    }
1661}
1662
1663pub(crate) fn json_to_vm_value(jv: &serde_json::Value) -> VmValue {
1664    match jv {
1665        serde_json::Value::Null => VmValue::Nil,
1666        serde_json::Value::Bool(b) => VmValue::Bool(*b),
1667        serde_json::Value::Number(n) => {
1668            if let Some(i) = n.as_i64() {
1669                VmValue::Int(i)
1670            } else {
1671                VmValue::Float(n.as_f64().unwrap_or(0.0))
1672            }
1673        }
1674        serde_json::Value::String(s) => VmValue::String(Rc::from(s.as_str())),
1675        serde_json::Value::Array(arr) => {
1676            VmValue::List(Rc::new(arr.iter().map(json_to_vm_value).collect()))
1677        }
1678        serde_json::Value::Object(map) => {
1679            let mut m = BTreeMap::new();
1680            for (k, v) in map {
1681                m.insert(k.clone(), json_to_vm_value(v));
1682            }
1683            VmValue::Dict(Rc::new(m))
1684        }
1685    }
1686}
1687
1688// =============================================================================
1689// Helper: validate a VmValue against a schema dict
1690// =============================================================================
1691
1692fn validate_value(
1693    value: &VmValue,
1694    schema: &BTreeMap<String, VmValue>,
1695    path: &str,
1696    errors: &mut Vec<String>,
1697) {
1698    // Check "type" constraint
1699    if let Some(VmValue::String(expected_type)) = schema.get("type") {
1700        let actual_type = value.type_name();
1701        let type_str: &str = expected_type;
1702        if type_str != "any" && actual_type != type_str {
1703            let location = if path.is_empty() {
1704                "root".to_string()
1705            } else {
1706                path.to_string()
1707            };
1708            errors.push(format!(
1709                "at {}: expected type '{}', got '{}'",
1710                location, type_str, actual_type
1711            ));
1712            return; // No point checking further if type is wrong
1713        }
1714    }
1715
1716    // Check "required" keys (only for dicts)
1717    if let Some(VmValue::List(required_keys)) = schema.get("required") {
1718        if let VmValue::Dict(map) = value {
1719            for key_val in required_keys.iter() {
1720                let key = key_val.display();
1721                if !map.contains_key(&key) {
1722                    let location = if path.is_empty() {
1723                        "root".to_string()
1724                    } else {
1725                        path.to_string()
1726                    };
1727                    errors.push(format!("at {}: missing required key '{}'", location, key));
1728                }
1729            }
1730        }
1731    }
1732
1733    // Check "properties" (only for dicts)
1734    if let Some(VmValue::Dict(prop_schemas)) = schema.get("properties") {
1735        if let VmValue::Dict(map) = value {
1736            for (key, prop_schema) in prop_schemas.iter() {
1737                if let Some(prop_value) = map.get(key) {
1738                    if let Some(prop_schema_dict) = prop_schema.as_dict() {
1739                        let child_path = if path.is_empty() {
1740                            key.clone()
1741                        } else {
1742                            format!("{}.{}", path, key)
1743                        };
1744                        validate_value(prop_value, prop_schema_dict, &child_path, errors);
1745                    }
1746                }
1747            }
1748        }
1749    }
1750
1751    // Check "items" (only for lists)
1752    if let Some(VmValue::Dict(item_schema)) = schema.get("items") {
1753        if let VmValue::List(items) = value {
1754            for (i, item) in items.iter().enumerate() {
1755                let child_path = if path.is_empty() {
1756                    format!("[{}]", i)
1757                } else {
1758                    format!("{}[{}]", path, i)
1759                };
1760                validate_value(item, item_schema, &child_path, errors);
1761            }
1762        }
1763    }
1764}
1765
1766// =============================================================================
1767// Helper: extract JSON from text with possible markdown fences
1768// =============================================================================
1769
1770fn extract_json_from_text(text: &str) -> String {
1771    let trimmed = text.trim();
1772
1773    // Try to find ```json ... ``` or ``` ... ``` code fences
1774    if let Some(start) = trimmed.find("```") {
1775        let after_backticks = &trimmed[start + 3..];
1776        // Skip optional language tag (e.g., "json")
1777        let content_start = if let Some(nl) = after_backticks.find('\n') {
1778            nl + 1
1779        } else {
1780            0
1781        };
1782        let content = &after_backticks[content_start..];
1783        if let Some(end) = content.find("```") {
1784            return content[..end].trim().to_string();
1785        }
1786    }
1787
1788    // No code fences found; try to find JSON object or array boundaries
1789    // Look for first { or [ and last matching } or ]
1790    if let Some(obj_start) = trimmed.find('{') {
1791        if let Some(obj_end) = trimmed.rfind('}') {
1792            if obj_end > obj_start {
1793                return trimmed[obj_start..=obj_end].to_string();
1794            }
1795        }
1796    }
1797    if let Some(arr_start) = trimmed.find('[') {
1798        if let Some(arr_end) = trimmed.rfind(']') {
1799            if arr_end > arr_start {
1800                return trimmed[arr_start..=arr_end].to_string();
1801            }
1802        }
1803    }
1804
1805    // Fall back to trimmed text as-is
1806    trimmed.to_string()
1807}
1808
1809// =============================================================================
1810// Helper: convert process::Output to VmValue dict
1811// =============================================================================
1812
1813fn vm_output_to_value(output: std::process::Output) -> VmValue {
1814    let mut result = BTreeMap::new();
1815    result.insert(
1816        "stdout".to_string(),
1817        VmValue::String(Rc::from(
1818            String::from_utf8_lossy(&output.stdout).to_string().as_str(),
1819        )),
1820    );
1821    result.insert(
1822        "stderr".to_string(),
1823        VmValue::String(Rc::from(
1824            String::from_utf8_lossy(&output.stderr).to_string().as_str(),
1825        )),
1826    );
1827    result.insert(
1828        "status".to_string(),
1829        VmValue::Int(output.status.code().unwrap_or(-1) as i64),
1830    );
1831    result.insert(
1832        "success".to_string(),
1833        VmValue::Bool(output.status.success()),
1834    );
1835    VmValue::Dict(Rc::new(result))
1836}
1837
1838// =============================================================================
1839// Helper: civil date from timestamp (Howard Hinnant's algorithm)
1840// =============================================================================
1841
1842fn vm_civil_from_timestamp(total_secs: u64) -> (i64, i64, i64, i64, i64, i64, i64) {
1843    let days = total_secs / 86400;
1844    let time_of_day = total_secs % 86400;
1845    let hour = (time_of_day / 3600) as i64;
1846    let minute = ((time_of_day % 3600) / 60) as i64;
1847    let second = (time_of_day % 60) as i64;
1848
1849    let z = days as i64 + 719468;
1850    let era = if z >= 0 { z } else { z - 146096 } / 146097;
1851    let doe = (z - era * 146097) as u64;
1852    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
1853    let y = yoe as i64 + era * 400;
1854    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
1855    let mp = (5 * doy + 2) / 153;
1856    let d = (doy - (153 * mp + 2) / 5 + 1) as i64;
1857    let m = if mp < 10 { mp + 3 } else { mp - 9 } as i64;
1858    let y = if m <= 2 { y + 1 } else { y };
1859    let dow = ((days + 4) % 7) as i64;
1860
1861    (y, m, d, hour, minute, second, dow)
1862}
1863
1864// =============================================================================
1865// Logging helpers for VM
1866// =============================================================================
1867
1868pub(crate) static VM_MIN_LOG_LEVEL: AtomicU8 = AtomicU8::new(0);
1869
1870#[derive(Clone)]
1871pub(crate) struct VmTraceContext {
1872    pub(crate) trace_id: String,
1873    pub(crate) span_id: String,
1874}
1875
1876thread_local! {
1877    pub(crate) static VM_TRACE_STACK: std::cell::RefCell<Vec<VmTraceContext>> = const { std::cell::RefCell::new(Vec::new()) };
1878}
1879
1880fn vm_level_to_u8(level: &str) -> Option<u8> {
1881    match level {
1882        "debug" => Some(0),
1883        "info" => Some(1),
1884        "warn" => Some(2),
1885        "error" => Some(3),
1886        _ => None,
1887    }
1888}
1889
1890fn vm_format_timestamp_utc() -> String {
1891    let now = std::time::SystemTime::now()
1892        .duration_since(std::time::UNIX_EPOCH)
1893        .unwrap_or_default();
1894    let total_secs = now.as_secs();
1895    let millis = now.subsec_millis();
1896
1897    let days = total_secs / 86400;
1898    let time_of_day = total_secs % 86400;
1899    let hour = time_of_day / 3600;
1900    let minute = (time_of_day % 3600) / 60;
1901    let second = time_of_day % 60;
1902
1903    let z = days as i64 + 719468;
1904    let era = if z >= 0 { z } else { z - 146096 } / 146097;
1905    let doe = (z - era * 146097) as u64;
1906    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
1907    let y = yoe as i64 + era * 400;
1908    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
1909    let mp = (5 * doy + 2) / 153;
1910    let d = doy - (153 * mp + 2) / 5 + 1;
1911    let m = if mp < 10 { mp + 3 } else { mp - 9 };
1912    let y = if m <= 2 { y + 1 } else { y };
1913
1914    format!("{y:04}-{m:02}-{d:02}T{hour:02}:{minute:02}:{second:02}.{millis:03}Z")
1915}
1916
1917pub(crate) fn vm_escape_json_str(s: &str) -> String {
1918    let mut out = String::with_capacity(s.len());
1919    for ch in s.chars() {
1920        match ch {
1921            '"' => out.push_str("\\\""),
1922            '\\' => out.push_str("\\\\"),
1923            '\n' => out.push_str("\\n"),
1924            '\r' => out.push_str("\\r"),
1925            '\t' => out.push_str("\\t"),
1926            c if c.is_control() => {
1927                out.push_str(&format!("\\u{:04x}", c as u32));
1928            }
1929            c => out.push(c),
1930        }
1931    }
1932    out
1933}
1934
1935fn vm_escape_json_str_quoted(s: &str) -> String {
1936    let mut out = String::with_capacity(s.len() + 2);
1937    out.push('"');
1938    out.push_str(&vm_escape_json_str(s));
1939    out.push('"');
1940    out
1941}
1942
1943fn vm_value_to_json_fragment(val: &VmValue) -> String {
1944    match val {
1945        VmValue::String(s) => vm_escape_json_str_quoted(s),
1946        VmValue::Int(n) => n.to_string(),
1947        VmValue::Float(n) => {
1948            if n.is_finite() {
1949                n.to_string()
1950            } else {
1951                "null".to_string()
1952            }
1953        }
1954        VmValue::Bool(b) => b.to_string(),
1955        VmValue::Nil => "null".to_string(),
1956        _ => vm_escape_json_str_quoted(&val.display()),
1957    }
1958}
1959
1960fn vm_build_log_line(level: &str, msg: &str, fields: Option<&BTreeMap<String, VmValue>>) -> String {
1961    let ts = vm_format_timestamp_utc();
1962    let mut parts: Vec<String> = Vec::new();
1963    parts.push(format!("\"ts\":{}", vm_escape_json_str_quoted(&ts)));
1964    parts.push(format!("\"level\":{}", vm_escape_json_str_quoted(level)));
1965    parts.push(format!("\"msg\":{}", vm_escape_json_str_quoted(msg)));
1966
1967    VM_TRACE_STACK.with(|stack| {
1968        if let Some(trace) = stack.borrow().last() {
1969            parts.push(format!(
1970                "\"trace_id\":{}",
1971                vm_escape_json_str_quoted(&trace.trace_id)
1972            ));
1973            parts.push(format!(
1974                "\"span_id\":{}",
1975                vm_escape_json_str_quoted(&trace.span_id)
1976            ));
1977        }
1978    });
1979
1980    if let Some(dict) = fields {
1981        for (k, v) in dict {
1982            parts.push(format!(
1983                "{}:{}",
1984                vm_escape_json_str_quoted(k),
1985                vm_value_to_json_fragment(v)
1986            ));
1987        }
1988    }
1989
1990    format!("{{{}}}\n", parts.join(","))
1991}
1992
1993fn vm_write_log(level: &str, level_num: u8, args: &[VmValue], out: &mut String) {
1994    if level_num < VM_MIN_LOG_LEVEL.load(Ordering::Relaxed) {
1995        return;
1996    }
1997    let msg = args.first().map(|a| a.display()).unwrap_or_default();
1998    let fields = args.get(1).and_then(|v| {
1999        if let VmValue::Dict(d) = v {
2000            Some(&**d)
2001        } else {
2002            None
2003        }
2004    });
2005    let line = vm_build_log_line(level, &msg, fields);
2006    out.push_str(&line);
2007}
2008
2009// =============================================================================
2010// Tool registry helpers for VM
2011// =============================================================================
2012
2013fn vm_validate_registry(name: &str, dict: &BTreeMap<String, VmValue>) -> Result<(), VmError> {
2014    match dict.get("_type") {
2015        Some(VmValue::String(t)) if &**t == "tool_registry" => Ok(()),
2016        _ => Err(VmError::Thrown(VmValue::String(Rc::from(format!(
2017            "{name}: argument must be a tool registry (created with tool_registry())"
2018        ))))),
2019    }
2020}
2021
2022fn vm_get_tools(dict: &BTreeMap<String, VmValue>) -> &[VmValue] {
2023    match dict.get("tools") {
2024        Some(VmValue::List(list)) => list,
2025        _ => &[],
2026    }
2027}
2028
2029fn vm_format_parameters(params: Option<&VmValue>) -> String {
2030    match params {
2031        Some(VmValue::Dict(map)) if !map.is_empty() => {
2032            let mut pairs: Vec<(String, String)> =
2033                map.iter().map(|(k, v)| (k.clone(), v.display())).collect();
2034            pairs.sort_by(|a, b| a.0.cmp(&b.0));
2035            pairs
2036                .iter()
2037                .map(|(k, v)| format!("{k}: {v}"))
2038                .collect::<Vec<_>>()
2039                .join(", ")
2040        }
2041        _ => String::new(),
2042    }
2043}
2044
2045fn vm_build_empty_schema() -> BTreeMap<String, VmValue> {
2046    let mut schema = BTreeMap::new();
2047    schema.insert(
2048        "schema_version".to_string(),
2049        VmValue::String(Rc::from("harn-tools/1.0")),
2050    );
2051    schema.insert("tools".to_string(), VmValue::List(Rc::new(Vec::new())));
2052    schema
2053}
2054
2055fn vm_build_input_schema(
2056    params: Option<&VmValue>,
2057    components: Option<&BTreeMap<String, VmValue>>,
2058) -> VmValue {
2059    let mut schema = BTreeMap::new();
2060    schema.insert("type".to_string(), VmValue::String(Rc::from("object")));
2061
2062    let params_map = match params {
2063        Some(VmValue::Dict(map)) if !map.is_empty() => map,
2064        _ => {
2065            schema.insert(
2066                "properties".to_string(),
2067                VmValue::Dict(Rc::new(BTreeMap::new())),
2068            );
2069            return VmValue::Dict(Rc::new(schema));
2070        }
2071    };
2072
2073    let mut properties = BTreeMap::new();
2074    let mut required = Vec::new();
2075
2076    for (key, val) in params_map.iter() {
2077        let prop = vm_resolve_param_type(val, components);
2078        properties.insert(key.clone(), prop);
2079        required.push(VmValue::String(Rc::from(key.as_str())));
2080    }
2081
2082    schema.insert("properties".to_string(), VmValue::Dict(Rc::new(properties)));
2083    if !required.is_empty() {
2084        required.sort_by_key(|a| a.display());
2085        schema.insert("required".to_string(), VmValue::List(Rc::new(required)));
2086    }
2087
2088    VmValue::Dict(Rc::new(schema))
2089}
2090
2091fn vm_resolve_param_type(val: &VmValue, components: Option<&BTreeMap<String, VmValue>>) -> VmValue {
2092    match val {
2093        VmValue::String(type_name) => {
2094            let json_type = vm_harn_type_to_json_schema(type_name);
2095            let mut prop = BTreeMap::new();
2096            prop.insert("type".to_string(), VmValue::String(Rc::from(json_type)));
2097            VmValue::Dict(Rc::new(prop))
2098        }
2099        VmValue::Dict(map) => {
2100            if let Some(VmValue::String(ref_name)) = map.get("$ref") {
2101                if let Some(comps) = components {
2102                    if let Some(resolved) = comps.get(&**ref_name) {
2103                        return resolved.clone();
2104                    }
2105                }
2106                let mut prop = BTreeMap::new();
2107                prop.insert(
2108                    "$ref".to_string(),
2109                    VmValue::String(Rc::from(
2110                        format!("#/components/schemas/{ref_name}").as_str(),
2111                    )),
2112                );
2113                VmValue::Dict(Rc::new(prop))
2114            } else {
2115                VmValue::Dict(Rc::new((**map).clone()))
2116            }
2117        }
2118        _ => {
2119            let mut prop = BTreeMap::new();
2120            prop.insert("type".to_string(), VmValue::String(Rc::from("string")));
2121            VmValue::Dict(Rc::new(prop))
2122        }
2123    }
2124}
2125
2126fn vm_harn_type_to_json_schema(harn_type: &str) -> &str {
2127    match harn_type {
2128        "int" => "integer",
2129        "float" => "number",
2130        "bool" | "boolean" => "boolean",
2131        "list" | "array" => "array",
2132        "dict" | "object" => "object",
2133        _ => "string",
2134    }
2135}