sim-lib-agent 0.1.2

Agent runtime surfaces for SIM.
Documentation
use super::support::{
    eval_cx, install_agent_lib, install_test_codec, register_sum_tool, temp_memory_path,
};
use crate::tools::tool_export_kind;
use crate::{AGENT_LIB_ID, FILE_WRITE_CAPABILITY, FileMemory, MemoryBackend, WorkingMemory};
use sim_kernel::{Error, Expr, Symbol};
use sim_lib_server::EvalSite;

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

    let tools = cx
        .eval_expr(Expr::Call {
            operator: Box::new(Expr::Symbol(Symbol::qualified("agent", "tools"))),
            args: vec![
                Expr::Symbol(Symbol::new(":category")),
                Expr::Quote {
                    mode: sim_kernel::QuoteMode::Quote,
                    expr: Box::new(Expr::Symbol(Symbol::new("math"))),
                },
            ],
        })
        .unwrap();
    let Expr::List(items) = tools.object().as_expr(&mut cx).unwrap() else {
        panic!("agent/tools should return a list");
    };
    assert_eq!(items.len(), 1);
    let Expr::Map(entries) = &items[0] else {
        panic!("tool metadata should be a table");
    };
    assert!(entries.iter().any(|(key, value)| {
        *key == Expr::Symbol(Symbol::new("category")) && *value == Expr::Symbol(Symbol::new("math"))
    }));

    let loaded = cx.registry().lib(&Symbol::new(AGENT_LIB_ID)).unwrap();
    assert!(loaded.exports.iter().any(|record| {
        record.kind == tool_export_kind()
            && record.symbol == Symbol::qualified("test", "sum")
            && matches!(record.state, sim_kernel::ExportState::Resolved { .. })
    }));
}

#[test]
fn tool_call_and_eval_site_answer_use_shape_checked_args() {
    let mut cx = eval_cx();
    install_test_codec(&mut cx);
    install_agent_lib(&mut cx).unwrap();
    let tool = register_sum_tool(&mut cx);

    let denied = cx
        .call_function(
            &Symbol::qualified("agent", "call-tool"),
            sim_kernel::Args::new(vec![
                cx.factory()
                    .symbol(Symbol::qualified("test", "sum"))
                    .unwrap(),
                cx.factory()
                    .number_literal(Symbol::qualified("numbers", "f64"), "2".to_owned())
                    .unwrap(),
                cx.factory()
                    .number_literal(Symbol::qualified("numbers", "f64"), "3".to_owned())
                    .unwrap(),
            ]),
        )
        .unwrap_err();
    assert!(matches!(
        denied,
        Error::CapabilityDenied { capability }
            if capability == sim_kernel::CapabilityName::new("math")
    ));
    cx.grant_named("math");

    let sum = cx
        .call_function(
            &Symbol::qualified("agent", "call-tool"),
            sim_kernel::Args::new(vec![
                cx.factory()
                    .symbol(Symbol::qualified("test", "sum"))
                    .unwrap(),
                cx.factory()
                    .number_literal(Symbol::qualified("numbers", "f64"), "2".to_owned())
                    .unwrap(),
                cx.factory()
                    .number_literal(Symbol::qualified("numbers", "f64"), "3".to_owned())
                    .unwrap(),
            ]),
        )
        .unwrap();
    assert_eq!(
        sum.object().as_expr(&mut cx).unwrap(),
        Expr::Number(sim_kernel::NumberLiteral {
            domain: Symbol::qualified("numbers", "f64"),
            canonical: "5".to_owned(),
        })
    );

    let request = sim_kernel::EvalRequest {
        expr: Expr::List(vec![
            Expr::Number(sim_kernel::NumberLiteral {
                domain: Symbol::qualified("numbers", "f64"),
                canonical: "4".to_owned(),
            }),
            Expr::Number(sim_kernel::NumberLiteral {
                domain: Symbol::qualified("numbers", "f64"),
                canonical: "6".to_owned(),
            }),
        ]),
        mode: sim_kernel::EvalMode::Eval,
        result_shape: None,
        answer_limit: None,
        stream_buffer: None,
        stream: false,
        required_capabilities: Vec::new(),
        deadline: None,
        consistency: sim_kernel::Consistency::LocalFirst,
        trace: false,
    };
    let frame = sim_lib_server::server_frame_from_request(
        &mut cx,
        &Symbol::qualified("codec", "binary"),
        request,
    )
    .unwrap();
    let reply = tool.answer(&mut cx, frame).unwrap();
    let reply = sim_lib_server::eval_reply_from_frame(&mut cx, &reply).unwrap();
    assert_eq!(
        reply.value.object().as_expr(&mut cx).unwrap(),
        Expr::Number(sim_kernel::NumberLiteral {
            domain: Symbol::qualified("numbers", "f64"),
            canonical: "10".to_owned(),
        })
    );
}

#[test]
fn tool_metadata_includes_shape_values() {
    let mut cx = eval_cx();
    install_test_codec(&mut cx);
    install_agent_lib(&mut cx).unwrap();
    let tool = register_sum_tool(&mut cx);

    let entries = tool.metadata_entries(&mut cx).unwrap();
    let args = entries
        .iter()
        .find_map(|(key, value)| (*key == Symbol::new("args")).then(|| value.clone()))
        .unwrap();
    assert!(args.object().as_shape().is_some());
}

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

    let memory = cx
        .call_function(
            &Symbol::qualified("memory", "working"),
            sim_kernel::Args::default(),
        )
        .unwrap();
    cx.call_function(
        &Symbol::qualified("memory", "append"),
        sim_kernel::Args::new(vec![
            memory.clone(),
            cx.factory().string("alpha".to_owned()).unwrap(),
        ]),
    )
    .unwrap();
    cx.call_function(
        &Symbol::qualified("memory", "append"),
        sim_kernel::Args::new(vec![
            memory.clone(),
            cx.factory().string("beta signal".to_owned()).unwrap(),
        ]),
    )
    .unwrap();

    let recent = cx
        .call_function(
            &Symbol::qualified("memory", "recent"),
            sim_kernel::Args::new(vec![
                memory.clone(),
                cx.factory()
                    .number_literal(Symbol::qualified("numbers", "f64"), "1".to_owned())
                    .unwrap(),
            ]),
        )
        .unwrap();
    assert_eq!(
        recent.object().as_expr(&mut cx).unwrap(),
        Expr::List(vec![Expr::String("beta signal".to_owned())])
    );

    let request = sim_kernel::EvalRequest {
        expr: Expr::List(vec![
            Expr::Symbol(Symbol::new("search")),
            Expr::List(vec![
                Expr::Symbol(Symbol::new("query")),
                Expr::String("signal".to_owned()),
            ]),
            Expr::Number(sim_kernel::NumberLiteral {
                domain: Symbol::qualified("numbers", "f64"),
                canonical: "3".to_owned(),
            }),
        ]),
        mode: sim_kernel::EvalMode::Eval,
        result_shape: None,
        answer_limit: None,
        stream_buffer: None,
        stream: false,
        required_capabilities: Vec::new(),
        deadline: None,
        consistency: sim_kernel::Consistency::LocalFirst,
        trace: false,
    };
    let frame = sim_lib_server::server_frame_from_request(
        &mut cx,
        &Symbol::qualified("codec", "binary"),
        request,
    )
    .unwrap();
    let reply = memory
        .object()
        .downcast_ref::<WorkingMemory>()
        .unwrap()
        .answer(&mut cx, frame)
        .unwrap();
    let reply = sim_lib_server::eval_reply_from_frame(&mut cx, &reply).unwrap();
    assert_eq!(
        reply.value.object().as_expr(&mut cx).unwrap(),
        Expr::List(vec![Expr::String("beta signal".to_owned())])
    );
}

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

    let path = temp_memory_path("requires-file-write");
    let denied = cx
        .call_function(
            &Symbol::qualified("memory", "file"),
            sim_kernel::Args::new(vec![
                cx.factory().string(path.display().to_string()).unwrap(),
            ]),
        )
        .unwrap_err();
    assert!(matches!(
        denied,
        Error::CapabilityDenied { capability }
            if capability == sim_kernel::CapabilityName::new(FILE_WRITE_CAPABILITY)
    ));

    cx.grant_named(FILE_WRITE_CAPABILITY);
    let memory = cx
        .call_function(
            &Symbol::qualified("memory", "file"),
            sim_kernel::Args::new(vec![
                cx.factory().string(path.display().to_string()).unwrap(),
            ]),
        )
        .unwrap();
    assert!(memory.object().downcast_ref::<FileMemory>().is_some());
    let _ = std::fs::remove_file(path);
}

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

    let path = temp_memory_path("episodic");
    let memory = FileMemory::open(&path, vec![Symbol::qualified("codec", "binary")]).unwrap();
    let persisted = cx.factory().string("persisted".to_owned()).unwrap();
    cx.grant_named(FILE_WRITE_CAPABILITY);
    memory.append(&mut cx, persisted).unwrap();
    let reopened = FileMemory::open(&path, vec![Symbol::qualified("codec", "binary")]).unwrap();
    assert_eq!(
        reopened.snapshot(&mut cx).unwrap(),
        Expr::List(vec![Expr::String("persisted".to_owned())])
    );

    let left = crate::BlackboardMemory::new(
        "ephemeral".to_owned(),
        vec![Symbol::qualified("codec", "binary")],
    );
    let right = crate::BlackboardMemory::new(
        "ephemeral".to_owned(),
        vec![Symbol::qualified("codec", "binary")],
    );
    let shared = cx.factory().string("shared".to_owned()).unwrap();
    left.append(&mut cx, shared).unwrap();
    assert_eq!(
        right.snapshot(&mut cx).unwrap(),
        Expr::List(vec![Expr::String("shared".to_owned())])
    );

    let _ = std::fs::remove_file(path);
}