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