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
14fn 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
26fn 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
35fn 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
58pub 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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", ®istry)?;
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 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 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 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 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 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 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 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 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 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 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 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 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 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
1903pub(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
1974fn validate_value(
1979 value: &VmValue,
1980 schema: &BTreeMap<String, VmValue>,
1981 path: &str,
1982 errors: &mut Vec<String>,
1983) {
1984 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; }
2000 }
2001
2002 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 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 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
2052fn extract_json_from_text(text: &str) -> String {
2057 let trimmed = text.trim();
2058
2059 if let Some(start) = trimmed.find("```") {
2061 let after_backticks = &trimmed[start + 3..];
2062 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 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 trimmed.to_string()
2093}
2094
2095fn 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
2124fn 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
2150pub(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
2295fn 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}