use rhai::{AST, Dynamic, Engine, Scope};
const REGION_HOOK_MAX_OPERATIONS: u64 = 100_000;
#[derive(Debug, Clone)]
pub struct RegionScript {
pub path: String,
ast: AST,
has_on_write: bool,
has_on_overflow: bool,
}
impl RegionScript {
pub fn has_on_write(&self) -> bool {
self.has_on_write
}
pub fn has_on_overflow(&self) -> bool {
self.has_on_overflow
}
}
fn build_engine() -> Engine {
let mut engine = Engine::new();
crate::harden(&mut engine, REGION_HOOK_MAX_OPERATIONS);
crate::functions::register_functions(&mut engine);
crate::types::register_types(&mut engine);
engine
}
pub fn compile(path: &str, source: &str) -> crate::Result<RegionScript> {
let engine = build_engine();
let ast = engine
.compile(source)
.map_err(|e| crate::Error::CompilationFailed(format!("{path}: {e}")))?;
let arity_of = |name: &str| -> Option<usize> {
ast.iter_functions()
.find(|f| f.name == name)
.map(|f| f.params.len())
};
match arity_of("render") {
Some(1) => {}
Some(n) => {
return Err(crate::Error::ValidationFailed(format!(
"{path}: fn render must take exactly one parameter (ctx), found {n}"
)));
}
None => {
return Err(crate::Error::ValidationFailed(format!(
"{path}: script must define fn render(ctx)"
)));
}
}
for optional in ["on_write", "on_overflow"] {
if let Some(n) = arity_of(optional)
&& n != 1
{
return Err(crate::Error::ValidationFailed(format!(
"{path}: fn {optional} must take exactly one parameter (ctx), found {n}"
)));
}
}
Ok(RegionScript {
path: path.to_string(),
has_on_write: arity_of("on_write").is_some(),
has_on_overflow: arity_of("on_overflow").is_some(),
ast,
})
}
fn call(
script: &RegionScript,
fn_name: &str,
ctx: serde_json::Value,
) -> crate::Result<serde_json::Value> {
let engine = build_engine();
let ctx_dyn = rhai::serde::to_dynamic(ctx).expect("JSON always converts to Dynamic");
let result: Dynamic = engine
.call_fn(&mut Scope::new(), &script.ast, fn_name, (ctx_dyn,))
.map_err(|e| crate::Error::ExecutionFailed(format!("{}: {fn_name}: {e}", script.path)))?;
rhai::serde::from_dynamic::<serde_json::Value>(&result).map_err(|e| {
crate::Error::ValidationFailed(format!(
"{}: {fn_name} returned a value that is not plain data: {e}",
script.path
))
})
}
pub fn run_render(
script: &RegionScript,
ctx: serde_json::Value,
) -> crate::Result<serde_json::Value> {
call(script, "render", ctx)
}
pub fn run_on_write(
script: &RegionScript,
ctx: serde_json::Value,
) -> crate::Result<serde_json::Value> {
call(script, "on_write", ctx)
}
pub fn run_on_overflow(
script: &RegionScript,
ctx: serde_json::Value,
) -> crate::Result<serde_json::Value> {
call(script, "on_overflow", ctx)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn ctx() -> serde_json::Value {
json!({
"region": { "name": "brain", "budget": 1000, "current_tokens": 10, "entry_count": 1 },
"entries": [ { "content": "hello", "tokens": 2, "kind": "text" } ],
"stage_name": "plan",
"stage_iterations": 3,
"model": "test-model",
"window": { "total_tokens": 10, "max_tokens": 2000 },
})
}
#[test]
fn compile_accepts_render_only_script() {
let s = compile("r.rhai", "fn render(ctx) { \"out\" }").unwrap();
assert_eq!(s.path, "r.rhai");
assert!(!s.has_on_write());
assert!(!s.has_on_overflow());
}
#[test]
fn compile_detects_optional_hooks() {
let src = r#"
fn render(ctx) { "out" }
fn on_write(ctx) { true }
fn on_overflow(ctx) { [] }
"#;
let s = compile("r.rhai", src).unwrap();
assert!(s.has_on_write());
assert!(s.has_on_overflow());
}
#[test]
fn compile_rejects_syntax_errors() {
let err = compile("bad.rhai", "fn render(ctx) {").unwrap_err();
assert!(
err.to_string().starts_with("Script compilation failed"),
"{err}"
);
assert!(err.to_string().contains("bad.rhai"));
}
#[test]
fn compile_rejects_missing_render() {
let err = compile("no.rhai", "fn on_write(ctx) { true }").unwrap_err();
assert!(
err.to_string().starts_with("Script validation failed"),
"{err}"
);
assert!(err.to_string().contains("must define fn render"));
}
#[test]
fn compile_rejects_wrong_render_arity() {
let err = compile("a.rhai", "fn render(a, b) { \"x\" }").unwrap_err();
assert!(err.to_string().contains("exactly one parameter"), "{err}");
}
#[test]
fn compile_rejects_wrong_optional_hook_arity() {
let src = "fn render(ctx) { \"x\" }\nfn on_write() { true }";
let err = compile("w.rhai", src).unwrap_err();
assert!(err.to_string().contains("on_write"), "{err}");
let src = "fn render(ctx) { \"x\" }\nfn on_overflow(a, b) { [] }";
let err = compile("o.rhai", src).unwrap_err();
assert!(err.to_string().contains("on_overflow"), "{err}");
}
#[test]
fn render_returns_string() {
let s = compile("r.rhai", "fn render(ctx) { `[${ctx.region.name}] ok` }").unwrap();
let out = run_render(&s, ctx()).unwrap();
assert_eq!(out, json!("[brain] ok"));
}
#[test]
fn render_sees_full_ctx() {
let src = r#"
fn render(ctx) {
#{
system: `${ctx.stage_name}/${ctx.stage_iterations}/${ctx.model}`,
messages: [
#{ role: "user", content: ctx.entries[0].content },
],
budget: ctx.region.budget,
window_max: ctx.window.max_tokens,
}
}
"#;
let s = compile("r.rhai", src).unwrap();
let out = run_render(&s, ctx()).unwrap();
assert_eq!(out["system"], json!("plan/3/test-model"));
assert_eq!(out["messages"][0]["content"], json!("hello"));
assert_eq!(out["budget"], json!(1000));
assert_eq!(out["window_max"], json!(2000));
}
#[test]
fn render_returns_unit_as_null() {
let s = compile("r.rhai", "fn render(ctx) { }").unwrap();
let out = run_render(&s, ctx()).unwrap();
assert_eq!(out, serde_json::Value::Null);
}
#[test]
fn render_runtime_throw_is_execution_error() {
let s = compile("r.rhai", "fn render(ctx) { throw \"boom\" }").unwrap();
let err = run_render(&s, ctx()).unwrap_err();
assert!(
err.to_string().starts_with("Script execution failed"),
"{err}"
);
assert!(err.to_string().contains("boom"), "{err}");
}
#[test]
fn render_infinite_loop_hits_operation_limit() {
let s = compile("r.rhai", "fn render(ctx) { loop { } }").unwrap();
let err = run_render(&s, ctx()).unwrap_err();
assert!(
err.to_string().starts_with("Script execution failed"),
"{err}"
);
}
#[test]
fn render_unrepresentable_return_is_validation_error() {
let s = compile("r.rhai", "fn render(ctx) { Fn(\"render\") }").unwrap();
let err = run_render(&s, ctx()).unwrap_err();
assert!(
err.to_string().starts_with("Script validation failed"),
"{err}"
);
}
#[test]
fn hooks_cannot_reach_disabled_engine_surface() {
let err = compile("r.rhai", "fn render(ctx) { eval(\"1+1\") }").unwrap_err();
assert!(
err.to_string().starts_with("Script compilation failed"),
"{err}"
);
assert!(err.to_string().contains("eval"), "{err}");
}
#[test]
fn on_write_returns_replacement_string() {
let src = r#"
fn render(ctx) { "" }
fn on_write(ctx) { ctx.entry.content.to_upper() }
"#;
let s = compile("w.rhai", src).unwrap();
let out = run_on_write(
&s,
json!({ "region": { "name": "brain" }, "entry": { "content": "hi", "kind": "text", "tokens": 1 }, "stage_name": "plan" }),
)
.unwrap();
assert_eq!(out, json!("HI"));
}
#[test]
fn on_write_returns_booleans_and_unit() {
for (body, expected) in [
("true", json!(true)),
("false", json!(false)),
("", serde_json::Value::Null),
] {
let src = format!("fn render(ctx) {{ \"\" }}\nfn on_write(ctx) {{ {body} }}");
let s = compile("w.rhai", &src).unwrap();
let out = run_on_write(&s, json!({"entry": {"content": "x"}})).unwrap();
assert_eq!(out, expected, "body: {body}");
}
}
#[test]
fn on_overflow_returns_index_array() {
let src = r#"
fn render(ctx) { "" }
fn on_overflow(ctx) {
let drops = [];
for (entry, i) in ctx.entries {
if entry.kind != "tool_result" { drops.push(i); }
}
drops
}
"#;
let s = compile("o.rhai", src).unwrap();
let out = run_on_overflow(
&s,
json!({
"region": { "name": "brain" },
"entries": [
{ "content": "a", "kind": "text" },
{ "content": "b", "kind": "tool_result" },
{ "content": "c", "kind": "text" },
],
"needed_tokens": 10,
}),
)
.unwrap();
assert_eq!(out, json!([0, 2]));
}
#[test]
fn calling_a_missing_hook_is_an_execution_error() {
let s = compile("r.rhai", "fn render(ctx) { \"\" }").unwrap();
assert!(!s.has_on_write());
let err = run_on_write(&s, json!({})).unwrap_err();
assert!(
err.to_string().starts_with("Script execution failed"),
"{err}"
);
}
}