sim-lib-agent 0.1.2

Agent runtime surfaces for SIM.
Documentation
use super::support::{as_component, eval_cx, flatten_text, install_agent_lib, request_frame};
use sim_codec_chat::validate_chat_transcript;
use sim_kernel::{Error, Expr, Symbol};
use sim_lib_server::{EvalSite, eval_reply_from_frame};

#[test]
fn a5_phase5_runner_process_json_stdio_replies_and_requires_capabilities() {
    let mut cx = eval_cx();
    super::support::install_roundtrip_codecs(&mut cx);
    install_agent_lib(&mut cx).unwrap();

    let runner = cx
        .call_function(
            &Symbol::qualified("runner", "process"),
            sim_kernel::Args::new(vec![
                cx.factory().symbol(Symbol::new(":name")).unwrap(),
                cx.factory().symbol(Symbol::new("local-json")).unwrap(),
                cx.factory().symbol(Symbol::new(":model")).unwrap(),
                cx.factory().string("local/stdin".to_owned()).unwrap(),
                cx.factory().symbol(Symbol::new(":command")).unwrap(),
                cx.factory()
                    .string(
                        "cat >/dev/null; printf '%s' '{\"$expr\":\"map\",\"entries\":[{\"key\":{\"$expr\":\"symbol\",\"name\":\"model-response\"},\"value\":{\"$expr\":\"bool\",\"value\":true}},{\"key\":{\"$expr\":\"symbol\",\"name\":\"runner\"},\"value\":{\"$expr\":\"symbol\",\"name\":\"local-json\"}},{\"key\":{\"$expr\":\"symbol\",\"name\":\"model\"},\"value\":{\"$expr\":\"string\",\"value\":\"local/stdin\"}},{\"key\":{\"$expr\":\"symbol\",\"name\":\"content\"},\"value\":{\"$expr\":\"list\",\"items\":[{\"$expr\":\"map\",\"entries\":[{\"key\":{\"$expr\":\"symbol\",\"name\":\"type\"},\"value\":{\"$expr\":\"symbol\",\"name\":\"text\"}},{\"key\":{\"$expr\":\"symbol\",\"name\":\"text\"},\"value\":{\"$expr\":\"string\",\"value\":\"fixture ok\"}}]}]}},{\"key\":{\"$expr\":\"symbol\",\"name\":\"stop-reason\"},\"value\":{\"$expr\":\"symbol\",\"name\":\"stop\"}}]}'".to_owned(),
                    )
                    .unwrap(),
            ]),
        )
        .unwrap();
    let request = request_frame(
        &mut cx,
        Expr::Map(vec![
            (Expr::Symbol(Symbol::new("model-request")), Expr::Bool(true)),
            (
                Expr::Symbol(Symbol::new("task")),
                Expr::String("hello".to_owned()),
            ),
            (
                Expr::Symbol(Symbol::new("messages")),
                Expr::List(Vec::new()),
            ),
        ]),
    );
    let denied = as_component(&runner)
        .answer(&mut cx, request.clone())
        .unwrap_err();
    assert!(matches!(
        denied,
        Error::CapabilityDenied { capability }
            if capability == sim_kernel::CapabilityName::new("ai-runner")
    ));

    cx.grant_named("ai-runner");
    cx.grant_named("ai-runner-local");
    cx.grant_named("host.process");
    let reply = as_component(&runner).answer(&mut cx, request).unwrap();
    let expr = eval_reply_from_frame(&mut cx, &reply)
        .unwrap()
        .value
        .object()
        .as_expr(&mut cx)
        .unwrap();
    validate_chat_transcript(&expr).unwrap();
    assert!(flatten_text(&expr).contains("fixture ok"));
}

#[test]
fn a5_phase5_runner_process_returns_error_transcripts_for_timeouts_and_oversize_output() {
    let mut cx = eval_cx();
    super::support::install_roundtrip_codecs(&mut cx);
    install_agent_lib(&mut cx).unwrap();
    cx.grant_named("ai-runner");
    cx.grant_named("ai-runner-local");
    cx.grant_named("host.process");

    let timed = cx
        .call_function(
            &Symbol::qualified("runner", "process"),
            sim_kernel::Args::new(vec![
                cx.factory().symbol(Symbol::new(":command")).unwrap(),
                cx.factory()
                    .string("sleep 1; printf late".to_owned())
                    .unwrap(),
                cx.factory().symbol(Symbol::new(":timeout")).unwrap(),
                cx.factory().string("50ms".to_owned()).unwrap(),
            ]),
        )
        .unwrap();
    let timeout_request = request_frame(&mut cx, request_expr("wait"));
    let timeout_reply = as_component(&timed)
        .answer(&mut cx, timeout_request)
        .unwrap();
    let timeout_expr = eval_reply_from_frame(&mut cx, &timeout_reply)
        .unwrap()
        .value
        .object()
        .as_expr(&mut cx)
        .unwrap();
    validate_chat_transcript(&timeout_expr).unwrap();
    assert!(flatten_text(&timeout_expr).contains("timed out after 50ms"));
    assert!(flatten_text(&timeout_expr).contains("error"));

    let oversized = cx
        .call_function(
            &Symbol::qualified("runner", "process"),
            sim_kernel::Args::new(vec![
                cx.factory().symbol(Symbol::new(":command")).unwrap(),
                cx.factory().string("printf 'abcdef'".to_owned()).unwrap(),
                cx.factory()
                    .symbol(Symbol::new(":max-output-bytes"))
                    .unwrap(),
                cx.factory()
                    .number_literal(Symbol::qualified("numbers", "f64"), "3".to_owned())
                    .unwrap(),
            ]),
        )
        .unwrap();
    let oversize_request = request_frame(&mut cx, request_expr("trim"));
    let oversize_reply = as_component(&oversized)
        .answer(&mut cx, oversize_request)
        .unwrap();
    let oversize_expr = eval_reply_from_frame(&mut cx, &oversize_reply)
        .unwrap()
        .value
        .object()
        .as_expr(&mut cx)
        .unwrap();
    validate_chat_transcript(&oversize_expr).unwrap();
    assert!(flatten_text(&oversize_expr).contains("exceeded max output bytes 3"));
    assert!(flatten_text(&oversize_expr).contains("error"));
}

fn request_expr(task: &str) -> Expr {
    Expr::Map(vec![
        (Expr::Symbol(Symbol::new("model-request")), Expr::Bool(true)),
        (
            Expr::Symbol(Symbol::new("task")),
            Expr::String(task.to_owned()),
        ),
        (
            Expr::Symbol(Symbol::new("messages")),
            Expr::List(Vec::new()),
        ),
    ])
}