sim-lib-agent 0.1.1

Agent runtime surfaces for SIM.
Documentation
use super::support::{
    as_component, eval_cx, flatten_text, install_agent_lib, install_roundtrip_codecs, request_frame,
};
use crate::tools::{Tool, register_tool};
use sim_kernel::{Args, Cx, Expr, Object, Result, Symbol, Value};
use sim_lib_server::{EvalSite, ServerAddress, eval_reply_from_frame};
use sim_shape::{AnyShape, shape_value};
use std::{any::Any, sync::Arc};

#[test]
fn r12_judges_compute_real_scores_rankings_and_rejections() {
    let mut cx = eval_cx();
    install_roundtrip_codecs(&mut cx);
    install_agent_lib(&mut cx).unwrap();

    let rubric = cx
        .call_function(
            &Symbol::qualified("judge", "rubric"),
            Args::new(vec![
                cx.factory().symbol(Symbol::new(":rubric")).unwrap(),
                cx.factory()
                    .expr(Expr::List(vec![
                        Expr::Symbol(Symbol::new(":correctness")),
                        number_expr(0.6),
                        Expr::Symbol(Symbol::new(":style")),
                        number_expr(0.2),
                        Expr::Symbol(Symbol::new(":brevity")),
                        number_expr(0.2),
                    ]))
                    .unwrap(),
                cx.factory().symbol(Symbol::new(":reference")).unwrap(),
                cx.factory()
                    .string("sim agents route work quickly".to_owned())
                    .unwrap(),
                cx.factory().symbol(Symbol::new(":style-markers")).unwrap(),
                cx.factory()
                    .expr(Expr::List(vec![Expr::String("quickly".to_owned())]))
                    .unwrap(),
            ]),
        )
        .unwrap();
    let rubric_request = request_frame(
        &mut cx,
        Expr::String("SIM agents route work quickly".to_owned()),
    );
    let rubric_reply = as_component(&rubric)
        .answer(&mut cx, rubric_request)
        .unwrap();
    let rubric_expr = eval_reply_from_frame(&mut cx, &rubric_reply)
        .unwrap()
        .value
        .object()
        .as_expr(&mut cx)
        .unwrap();
    let score = map_number_field(&rubric_expr, "score");
    assert!(score > 0.7, "rubric score too low: {score}");
    assert!(map_has_key(&rubric_expr, "breakdown"));

    let ranked = cx
        .call_function(
            &Symbol::qualified("judge", "ranked-vote"),
            Args::new(vec![
                cx.factory().symbol(Symbol::new(":rubric")).unwrap(),
                cx.factory()
                    .expr(Expr::List(vec![
                        Expr::Symbol(Symbol::new(":correctness")),
                        number_expr(0.7),
                        Expr::Symbol(Symbol::new(":brevity")),
                        number_expr(0.3),
                    ]))
                    .unwrap(),
                cx.factory().symbol(Symbol::new(":reference")).unwrap(),
                cx.factory().string("hello sim world".to_owned()).unwrap(),
            ]),
        )
        .unwrap();
    let ranked_request = request_frame(
        &mut cx,
        Expr::List(vec![
            Expr::String("hello sim world".to_owned()),
            Expr::String("hello world with extra padding words and clutter".to_owned()),
            Expr::String("unrelated output".to_owned()),
        ]),
    );
    let ranked_reply = as_component(&ranked)
        .answer(&mut cx, ranked_request)
        .unwrap();
    let ranked_expr = eval_reply_from_frame(&mut cx, &ranked_reply)
        .unwrap()
        .value
        .object()
        .as_expr(&mut cx)
        .unwrap();
    assert_eq!(
        map_expr_field(&ranked_expr, "winner"),
        Expr::String("hello sim world".to_owned())
    );
    assert!(flatten_text(&ranked_expr).contains("ranking"));

    let threshold = cx
        .call_function(
            &Symbol::qualified("judge", "threshold"),
            Args::new(vec![
                cx.factory().symbol(Symbol::new(":threshold")).unwrap(),
                cx.factory().expr(number_expr(0.8)).unwrap(),
                cx.factory().symbol(Symbol::new(":reference")).unwrap(),
                cx.factory()
                    .string("ship a precise concise answer".to_owned())
                    .unwrap(),
            ]),
        )
        .unwrap();
    let threshold_request =
        request_frame(&mut cx, Expr::String("rambling mismatch text".to_owned()));
    let threshold_reply = as_component(&threshold)
        .answer(&mut cx, threshold_request)
        .unwrap();
    let threshold_expr = eval_reply_from_frame(&mut cx, &threshold_reply)
        .unwrap()
        .value
        .object()
        .as_expr(&mut cx)
        .unwrap();
    assert!(!map_bool_field(&threshold_expr, "approved"));
}

#[test]
fn r12_router_bid_runs_real_auction_and_picks_best_worker() {
    let mut cx = eval_cx();
    install_roundtrip_codecs(&mut cx);
    install_agent_lib(&mut cx).unwrap();

    let cheap = register_bid_tool(&mut cx, Symbol::qualified("test", "cheap-bid"), 3.0);
    let expensive = register_bid_tool(&mut cx, Symbol::qualified("test", "expensive-bid"), 9.0);
    let _ = (cheap, expensive);

    let router = cx
        .call_function(
            &Symbol::qualified("router", "bid"),
            Args::new(vec![
                cx.factory().symbol(Symbol::new(":targets")).unwrap(),
                cx.factory()
                    .expr(Expr::List(vec![
                        Expr::Symbol(Symbol::qualified("test", "expensive-bid")),
                        Expr::Symbol(Symbol::qualified("test", "cheap-bid")),
                    ]))
                    .unwrap(),
                cx.factory().symbol(Symbol::new(":metric")).unwrap(),
                cx.factory().symbol(Symbol::new("estimated-cost")).unwrap(),
                cx.factory().symbol(Symbol::new(":auction-window")).unwrap(),
                cx.factory().string("200ms".to_owned()).unwrap(),
            ]),
        )
        .unwrap();

    let router_request = request_frame(&mut cx, Expr::String("route this task".to_owned()));
    let reply = as_component(&router)
        .answer(&mut cx, router_request)
        .unwrap();
    let expr = eval_reply_from_frame(&mut cx, &reply)
        .unwrap()
        .value
        .object()
        .as_expr(&mut cx)
        .unwrap();
    assert_eq!(
        map_expr_field(&expr, "target"),
        Expr::Symbol(Symbol::qualified("test", "cheap-bid"))
    );
    assert_eq!(map_number_field(&expr, "bid"), 3.0);
}

#[test]
fn r12_personas_apply_real_style_language_and_translation_rules() {
    let mut cx = eval_cx();
    install_roundtrip_codecs(&mut cx);
    install_agent_lib(&mut cx).unwrap();

    let style = cx
        .call_function(
            &Symbol::qualified("persona", "style"),
            Args::new(vec![
                cx.factory().symbol(Symbol::new(":voice")).unwrap(),
                cx.factory().string("terse".to_owned()).unwrap(),
                cx.factory().symbol(Symbol::new(":prefix")).unwrap(),
                cx.factory().string("SIM:".to_owned()).unwrap(),
                cx.factory().symbol(Symbol::new(":suffix")).unwrap(),
                cx.factory().string("[ok]".to_owned()).unwrap(),
            ]),
        )
        .unwrap();
    let style_request = request_frame(
        &mut cx,
        Expr::String(
            "SIM: explain the routing behavior with several extra filler words here [ok]"
                .to_owned(),
        ),
    );
    let style_reply = as_component(&style).answer(&mut cx, style_request).unwrap();
    let style_expr = eval_reply_from_frame(&mut cx, &style_reply)
        .unwrap()
        .value
        .object()
        .as_expr(&mut cx)
        .unwrap();
    let styled = expect_string(style_expr);
    assert!(styled.starts_with("SIM: "));
    assert!(styled.ends_with("[ok]"));
    assert!(styled.split_whitespace().count() <= 14);

    let language = cx
        .call_function(
            &Symbol::qualified("persona", "language"),
            Args::new(vec![
                cx.factory().symbol(Symbol::new(":language")).unwrap(),
                cx.factory().string("es".to_owned()).unwrap(),
            ]),
        )
        .unwrap();
    let language_request = request_frame(&mut cx, Expr::String("hello agent mystery".to_owned()));
    let language_reply = as_component(&language)
        .answer(&mut cx, language_request)
        .unwrap();
    let language_expr = eval_reply_from_frame(&mut cx, &language_reply)
        .unwrap()
        .value
        .object()
        .as_expr(&mut cx)
        .unwrap();
    let localized = expect_string(language_expr);
    assert!(localized.contains("hola"));
    assert!(localized.contains("agente"));
    assert!(localized.contains("[es:mystery]"));

    let translator = cx
        .call_function(
            &Symbol::qualified("persona", "translator"),
            Args::new(vec![
                cx.factory().symbol(Symbol::new(":from")).unwrap(),
                cx.factory().string("en".to_owned()).unwrap(),
                cx.factory().symbol(Symbol::new(":to")).unwrap(),
                cx.factory().string("sv".to_owned()).unwrap(),
                cx.factory().symbol(Symbol::new(":table")).unwrap(),
                cx.factory()
                    .expr(Expr::List(vec![
                        Expr::Symbol(Symbol::new(":hello")),
                        Expr::String("hej".to_owned()),
                        Expr::Symbol(Symbol::new(":world")),
                        Expr::String("varld".to_owned()),
                    ]))
                    .unwrap(),
            ]),
        )
        .unwrap();
    let translator_request = request_frame(&mut cx, Expr::String("hello world unknown".to_owned()));
    let translator_reply = as_component(&translator)
        .answer(&mut cx, translator_request)
        .unwrap();
    let translator_expr = eval_reply_from_frame(&mut cx, &translator_reply)
        .unwrap()
        .value
        .object()
        .as_expr(&mut cx)
        .unwrap();
    assert_eq!(
        map_expr_field(&translator_expr, "text"),
        Expr::String("hej varld unknown".to_owned())
    );
    assert!(map_number_field(&translator_expr, "coverage") > 0.6);
}

#[derive(Clone)]
struct FixedBidFn {
    bid: f64,
}

impl Object for FixedBidFn {
    fn display(&self, _cx: &mut Cx) -> Result<String> {
        Ok("#<function test/fixed-bid>".to_owned())
    }

    fn as_any(&self) -> &dyn Any {
        self
    }
}

impl sim_kernel::ObjectCompat for FixedBidFn {
    fn class(&self, cx: &mut Cx) -> Result<sim_kernel::ClassRef> {
        cx.factory().class_stub(
            sim_kernel::ClassId(0),
            Symbol::qualified("core", "Function"),
        )
    }
    fn as_callable(&self) -> Option<&dyn sim_kernel::Callable> {
        Some(self)
    }
}

impl sim_kernel::Callable for FixedBidFn {
    fn call(&self, cx: &mut Cx, _args: Args) -> Result<Value> {
        cx.factory()
            .number_literal(Symbol::qualified("numbers", "f64"), self.bid.to_string())
    }
}

fn register_bid_tool(cx: &mut Cx, symbol: Symbol, bid: f64) -> Arc<Tool> {
    let callable = cx.factory().opaque(Arc::new(FixedBidFn { bid })).unwrap();
    let tool = Arc::new(Tool {
        symbol: symbol.clone(),
        description: "fixed bid worker".to_owned(),
        args_shape: shape_value(Symbol::qualified("test", "bid-args"), Arc::new(AnyShape)),
        result_shape: None,
        category: Symbol::new("worker"),
        capabilities: Vec::new(),
        function: callable,
        address: ServerAddress::Local,
        codecs: vec![Symbol::qualified("codec", "binary")],
    });
    let value = cx.factory().opaque(tool.clone()).unwrap();
    register_tool(cx, tool.clone(), value).unwrap();
    tool
}

fn map_expr_field(expr: &Expr, key: &str) -> Expr {
    let Expr::Map(entries) = expr else {
        panic!("expected map expr, found {expr:?}");
    };
    entries
        .iter()
        .find_map(|(entry_key, entry_value)| match entry_key {
            Expr::Symbol(symbol) if symbol.name.as_ref() == key => Some(entry_value.clone()),
            _ => None,
        })
        .unwrap_or_else(|| panic!("missing key {key} in {expr:?}"))
}

fn map_number_field(expr: &Expr, key: &str) -> f64 {
    match map_expr_field(expr, key) {
        Expr::Number(number) => number.canonical.parse::<f64>().unwrap(),
        other => panic!("expected numeric field {key}, found {other:?}"),
    }
}

fn map_bool_field(expr: &Expr, key: &str) -> bool {
    match map_expr_field(expr, key) {
        Expr::Bool(value) => value,
        other => panic!("expected bool field {key}, found {other:?}"),
    }
}

fn map_has_key(expr: &Expr, key: &str) -> bool {
    let Expr::Map(entries) = expr else {
        return false;
    };
    entries.iter().any(
        |(entry_key, _)| matches!(entry_key, Expr::Symbol(symbol) if symbol.name.as_ref() == key),
    )
}

fn number_expr(value: f64) -> Expr {
    Expr::Number(sim_kernel::NumberLiteral {
        domain: Symbol::qualified("numbers", "f64"),
        canonical: value.to_string(),
    })
}

fn expect_string(expr: Expr) -> String {
    match expr {
        Expr::String(text) => text,
        other => panic!("expected string expr, found {other:?}"),
    }
}