sim-lib-agent 0.1.2

Agent runtime surfaces for SIM.
Documentation
use super::support::{eval_cx, install_agent_lib, install_roundtrip_codecs};
use crate::AgentPattern;
use sim_codec::{Input, decode_with_codec, encode_with_codec};
use sim_kernel::{Args, EncodeOptions, Expr, Lib, ReadPolicy, Symbol};

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

    let pattern = pattern_value(&mut cx);
    let descriptor = pattern.object().downcast_ref::<AgentPattern>().unwrap();
    assert_eq!(descriptor.id, Symbol::new("a30-001-autonomous-decision"));
    assert_eq!(descriptor.sense[0].name, Symbol::new("input"));

    let expr = pattern.object().as_expr(&mut cx).unwrap();
    assert_eq!(
        field_symbol(&expr, "kind"),
        Some(Symbol::new("agent-pattern"))
    );
    let card = field(&expr, "card").unwrap();
    assert_eq!(
        field_symbol(card, "kind"),
        Some(Symbol::new("agent-pattern"))
    );
    assert_eq!(
        list_symbols(field(card, "fields").unwrap()),
        vec![
            "sense",
            "model",
            "plan",
            "act",
            "memory",
            "tools",
            "policy",
            "evaluation",
            "guardrail",
            "trace",
        ]
    );
}

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

    let pattern = pattern_value(&mut cx);
    let expr = pattern.object().as_expr(&mut cx).unwrap();
    for codec in [
        Symbol::qualified("codec", "json"),
        Symbol::qualified("codec", "lisp"),
    ] {
        let encoded = encode_with_codec(&mut cx, &codec, &expr, EncodeOptions::default()).unwrap();
        let decoded = decode_with_codec(
            &mut cx,
            &codec,
            match encoded {
                sim_codec::Output::Text(text) => Input::Text(text),
                sim_codec::Output::Bytes(bytes) => Input::Bytes(bytes),
            },
            ReadPolicy::default(),
        )
        .unwrap();
        assert!(decoded.canonical_eq(&expr));
    }
}

#[test]
fn agent_pattern_function_is_exported() {
    let exports = crate::AgentLib.manifest().exports;
    assert!(exports.iter().any(|export| {
        matches!(
            export,
            sim_kernel::Export::Function { symbol, .. }
                if *symbol == Symbol::qualified("agent", "pattern")
        )
    }));
}

fn pattern_value(cx: &mut sim_kernel::Cx) -> sim_kernel::Value {
    let args = vec![
        symbol_value(cx, ":id"),
        symbol_value(cx, "a30-001-autonomous-decision"),
        symbol_value(cx, ":description"),
        cx.factory()
            .string("deterministic autonomous decision descriptor".to_owned())
            .unwrap(),
        symbol_value(cx, ":sense"),
        slot_list(cx, &["input", "policy"]),
        symbol_value(cx, ":model"),
        slot_list(cx, &["fake-runner"]),
        symbol_value(cx, ":plan"),
        slot_list(cx, &["threshold-check"]),
        symbol_value(cx, ":act"),
        slot_list(cx, &["approve-ticket"]),
        symbol_value(cx, ":memory"),
        slot_list(cx, &["none"]),
        symbol_value(cx, ":tools"),
        slot_list(cx, &["none"]),
        symbol_value(cx, ":policy"),
        slot_list(cx, &["low-risk-threshold"]),
        symbol_value(cx, ":evaluation"),
        slot_list(cx, &["deterministic-match"]),
        symbol_value(cx, ":guardrail"),
        slot_list(cx, &["offline-synthetic-fixture"]),
        symbol_value(cx, ":trace"),
        slot_list(cx, &["decision-record"]),
    ];
    cx.call_function(&Symbol::qualified("agent", "pattern"), Args::new(args))
        .unwrap()
}

fn symbol_value(cx: &mut sim_kernel::Cx, name: &str) -> sim_kernel::Value {
    cx.factory().symbol(Symbol::new(name)).unwrap()
}

fn slot_list(cx: &mut sim_kernel::Cx, names: &[&str]) -> sim_kernel::Value {
    let values = names
        .iter()
        .map(|name| symbol_value(cx, name))
        .collect::<Vec<_>>();
    cx.factory().list(values).unwrap()
}

fn field<'a>(expr: &'a Expr, name: &str) -> Option<&'a Expr> {
    let Expr::Map(entries) = expr else {
        return None;
    };
    entries.iter().find_map(|(key, value)| {
        matches!(key, Expr::Symbol(symbol) if symbol.name.as_ref() == name).then_some(value)
    })
}

fn field_symbol(expr: &Expr, name: &str) -> Option<Symbol> {
    match field(expr, name) {
        Some(Expr::Symbol(symbol)) => Some(symbol.clone()),
        _ => None,
    }
}

fn list_symbols(expr: &Expr) -> Vec<&str> {
    let Expr::List(items) = expr else {
        return Vec::new();
    };
    items
        .iter()
        .filter_map(|item| match item {
            Expr::Symbol(symbol) => Some(symbol.name.as_ref()),
            _ => None,
        })
        .collect()
}