sim-lib-agent 0.1.6

Agent runtime surfaces for SIM.
Documentation
use sim_config::{ConfigProbe, ConfigProbeCaps, ConfigProbeRequest, ConfigProbeStatus, ProbeMode};
use sim_kernel::{Args, Expr, Symbol};
use sim_lib_agent_runner_core::ModelCard;

use super::support::{eval_cx, install_agent_lib, install_test_codec};
use crate::{AgentModelConfigProbe, AgentModelProviderPresence, model_defaults_config_lib_symbol};

#[test]
fn config_probe_modeled_emits_fixture_defaults_without_environment() {
    let probe = AgentModelConfigProbe::modeled();
    let request = modeled_request();

    let (layer, report) = probe.probe(&request);

    assert_eq!(report.status, ConfigProbeStatus::Applied);
    assert_eq!(
        report.emitted_keys,
        [
            "model_regex",
            "provider_regex",
            "prefer_local",
            "default_model",
            "openai_key_present",
            "openai_base_present",
            "ollama_host_present",
        ]
    );
    let table = layer_table(layer.unwrap());
    assert_eq!(
        field(&table, "default_model"),
        &Expr::String("fixture/echo".to_owned())
    );
    assert_eq!(
        field(&table, "provider_regex"),
        &Expr::String(r"^(?:modeled)$".to_owned())
    );
    assert_eq!(field(&table, "prefer_local"), &Expr::Bool(true));
    assert_eq!(field(&table, "openai_key_present"), &Expr::Bool(false));
    assert_eq!(field(&table, "openai_base_present"), &Expr::Bool(false));
    assert_eq!(field(&table, "ollama_host_present"), &Expr::Bool(false));
}

#[test]
fn config_probe_real_requires_env_capability() {
    let probe = AgentModelConfigProbe::new(Vec::new(), AgentModelProviderPresence::default());
    let request = ConfigProbeRequest {
        mode: ProbeMode::Real,
        ..modeled_request()
    };

    let (layer, report) = probe.probe(&request);

    assert!(layer.is_none());
    assert_eq!(
        report.status,
        ConfigProbeStatus::Denied {
            capability: "env".to_owned()
        }
    );
    assert!(report.emitted_keys.is_empty());
}

#[test]
fn config_probe_real_selects_local_matching_card_deterministically() {
    let cards = vec![
        card(
            "runner/remote",
            "gpt-4.1-mini",
            "openai-compatible",
            "remote",
        ),
        card("runner/local", "sim/local-echo", "local-model", "local"),
        card("runner/other", "other/model", "other-provider", "local"),
    ];
    let probe = AgentModelConfigProbe::new(
        cards,
        AgentModelProviderPresence {
            openai_key_present: true,
            openai_base_present: true,
            ollama_host_present: false,
        },
    );
    let request = ConfigProbeRequest {
        mode: ProbeMode::Real,
        caps: ConfigProbeCaps {
            env: true,
            ..ConfigProbeCaps::default()
        },
        ..modeled_request()
    };

    let (layer, report) = probe.probe(&request);

    assert_eq!(report.status, ConfigProbeStatus::Applied);
    let table = layer_table(layer.unwrap());
    assert_eq!(
        field(&table, "default_model"),
        &Expr::String("sim/local-echo".to_owned())
    );
    assert_eq!(
        field(&table, "provider_regex"),
        &Expr::String(
            r"^(?:modeled|openai|openai-compatible|local-model|other-provider)$".to_owned()
        )
    );
    assert_eq!(field(&table, "openai_key_present"), &Expr::Bool(true));
    assert_eq!(field(&table, "openai_base_present"), &Expr::Bool(true));
    assert_eq!(field(&table, "ollama_host_present"), &Expr::Bool(false));
}

#[test]
fn config_probe_real_falls_back_when_model_regex_has_no_match() {
    let probe = AgentModelConfigProbe::new(
        vec![card(
            "runner/custom",
            "custom/model",
            "custom-provider",
            "remote",
        )],
        AgentModelProviderPresence {
            ollama_host_present: true,
            ..AgentModelProviderPresence::default()
        },
    );
    let request = ConfigProbeRequest {
        mode: ProbeMode::Real,
        caps: ConfigProbeCaps {
            env: true,
            ..ConfigProbeCaps::default()
        },
        ..modeled_request()
    };

    let (layer, report) = probe.probe(&request);

    assert_eq!(report.status, ConfigProbeStatus::Applied);
    let table = layer_table(layer.unwrap());
    assert_eq!(
        field(&table, "default_model"),
        &Expr::String("fixture/echo".to_owned())
    );
    assert_eq!(
        field(&table, "provider_regex"),
        &Expr::String(r"^(?:modeled|ollama|custom-provider)$".to_owned())
    );
}

#[test]
fn config_probe_skips_other_libs() {
    let probe = AgentModelConfigProbe::modeled();
    let request = ConfigProbeRequest {
        lib: Symbol::qualified("stream", "host"),
        ..modeled_request()
    };

    let (layer, report) = probe.probe(&request);

    assert!(layer.is_none());
    assert!(matches!(report.status, ConfigProbeStatus::Skipped { .. }));
}

#[test]
fn config_probe_defaults_can_drive_model_placement() {
    let mut cx = eval_cx();
    install_test_codec(&mut cx);
    install_agent_lib(&mut cx).unwrap();
    let runner = cx
        .call_function(
            &Symbol::qualified("runner", "echo"),
            Args::new(vec![
                cx.factory().symbol(Symbol::new(":name")).unwrap(),
                cx.factory()
                    .symbol(Symbol::new("configured-local"))
                    .unwrap(),
                cx.factory().symbol(Symbol::new(":model")).unwrap(),
                cx.factory()
                    .string("sim/configured-local".to_owned())
                    .unwrap(),
            ]),
        )
        .unwrap();
    let runner_list = cx.factory().list(vec![runner.clone()]).unwrap();
    let cards = cx
        .call_function(
            &Symbol::qualified("runner", "cards"),
            Args::new(vec![runner_list]),
        )
        .unwrap();
    let Expr::List(cards) = cards.object().as_expr(&mut cx).unwrap() else {
        panic!("runner/cards must return a list");
    };
    let cards = cards
        .into_iter()
        .map(ModelCard::try_from)
        .collect::<sim_kernel::Result<Vec<_>>>()
        .unwrap();
    let probe = AgentModelConfigProbe::new(cards, AgentModelProviderPresence::default());
    let request = ConfigProbeRequest {
        mode: ProbeMode::Real,
        caps: ConfigProbeCaps {
            env: true,
            ..ConfigProbeCaps::default()
        },
        ..modeled_request()
    };

    let (layer, _) = probe.probe(&request);
    let table = layer_table(layer.unwrap());
    assert_eq!(
        field(&table, "default_model"),
        &Expr::String("sim/configured-local".to_owned())
    );

    let placed = cx
        .call_function(
            &Symbol::qualified("runner", "place"),
            Args::new(vec![
                cx.factory()
                    .string("model-site:configured-local".to_owned())
                    .unwrap(),
                runner,
            ]),
        )
        .unwrap();
    let placed = placed.object().as_expr(&mut cx).unwrap();
    assert_eq!(
        maybe_field(&placed, "model"),
        Some(&Expr::String("sim/configured-local".to_owned()))
    );
}

fn modeled_request() -> ConfigProbeRequest {
    ConfigProbeRequest {
        lib: model_defaults_config_lib_symbol(),
        mode: ProbeMode::Modeled,
        caps: ConfigProbeCaps::default(),
    }
}

fn card(runner: &str, model: &str, provider: &str, locality: &str) -> ModelCard {
    ModelCard::new(
        symbol(runner),
        model,
        Symbol::new(provider),
        Symbol::new(locality),
    )
}

fn symbol(value: &str) -> Symbol {
    match value.split_once('/') {
        Some((namespace, name)) => Symbol::qualified(namespace, name),
        None => Symbol::new(value),
    }
}

fn layer_table(layer: sim_config::ConfigLayer) -> Expr {
    layer
        .dir
        .table(&model_defaults_config_lib_symbol())
        .unwrap()
        .table
        .clone()
}

fn field<'a>(expr: &'a Expr, name: &str) -> &'a Expr {
    maybe_field(expr, name).unwrap_or_else(|| panic!("missing field {name}"))
}

fn maybe_field<'a>(expr: &'a Expr, name: &str) -> Option<&'a Expr> {
    let Expr::Map(entries) = expr else {
        panic!("expected map");
    };
    entries.iter().find_map(|(key, value)| match key {
        Expr::Symbol(symbol) if symbol.name.as_ref() == name => Some(value),
        _ => None,
    })
}