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