sim-lib-agent 0.1.2

Agent runtime surfaces for SIM.
Documentation
use super::support::{
    as_component, eval_cx, flatten_text, install_agent_lib, install_test_codec, register_sum_tool,
    request_frame,
};
use sim_kernel::{Args, Cx, Expr, Result, Symbol, Value};
use sim_lib_server::{EvalSite, ServerFrame, eval_reply_from_frame};

#[test]
fn a6_phase6_local_only_permits_local_fake_runner() {
    let mut cx = privacy_cx();
    let runner = fake_runner(&mut cx, "local-fake", vec![final_response("local ok")]);
    let (expr, _) = runner_answer_expr(
        &mut cx,
        &runner,
        model_request("local prompt", vec![privacy_symbol("local-only")]),
    );

    assert!(flatten_text(&expr).contains("local ok"));
}

#[test]
fn a6_phase6_metadata_only_recorder_hashes_payload_text() {
    let mut cx = privacy_cx();
    cx.grant_named("agent-spawn");
    let recorder = recorder_journal(&mut cx);
    let runner = fake_runner(
        &mut cx,
        "private-fake",
        vec![final_response("secret response text")],
    );
    let agent = started_agent(&mut cx, vec![runner], Vec::new(), vec![recorder.clone()]);

    let (expr, _) = agent_answer_expr(
        &mut cx,
        &agent,
        model_request("secret prompt text", vec![privacy_symbol("metadata-only")]),
    );

    assert!(flatten_text(&expr).contains("secret response text"));
    let snapshot = recorder_snapshot(&mut cx, &recorder);
    let snapshot_text = format!("{snapshot:?}");
    assert!(snapshot_text.contains("privacy-redacted"));
    assert!(snapshot_text.contains("payload-hash"));
    assert!(!snapshot_text.contains("secret prompt text"));
    assert!(!snapshot_text.contains("secret response text"));
}

#[test]
fn a6_phase6_allow_tools_denies_prompt_content_to_unlisted_tool() {
    let mut cx = privacy_cx();
    cx.grant_named("agent-spawn");
    cx.grant_named("math");
    let tool = register_sum_tool(&mut cx);
    let tool_value = cx.resolve_value(&tool.symbol).unwrap();
    let runner = fake_runner(
        &mut cx,
        "tool-private-fake",
        vec![tool_call_response(vec![tool_call(
            "call-1",
            Symbol::qualified("test", "sum"),
            vec![number(2), number(3)],
        )])],
    );
    let agent = started_agent(&mut cx, vec![runner], vec![tool_value], Vec::new());

    let (expr, _) = agent_answer_expr(
        &mut cx,
        &agent,
        model_request(
            "private tool prompt",
            vec![key_expr(
                "privacy",
                Expr::Map(vec![key_expr("allow-tools", Expr::List(Vec::new()))]),
            )],
        ),
    );

    assert!(flatten_text(&expr).contains("privacy policy denied tool test/sum"));
}

fn privacy_cx() -> Cx {
    let mut cx = eval_cx();
    install_test_codec(&mut cx);
    install_agent_lib(&mut cx).unwrap();
    cx
}

fn runner_answer_expr(
    cx: &mut Cx,
    runner: &Value,
    request: Expr,
) -> (Expr, Vec<sim_kernel::Diagnostic>) {
    let frame = request_frame(cx, request);
    let reply = as_component(runner).answer(cx, frame).unwrap();
    let reply = eval_reply_from_frame(cx, &reply).unwrap();
    (reply.value.object().as_expr(cx).unwrap(), reply.diagnostics)
}

fn started_agent(
    cx: &mut Cx,
    runners: Vec<Value>,
    tools: Vec<Value>,
    recorders: Vec<Value>,
) -> Value {
    let runners = manifest_arg(cx, runners);
    let tools = manifest_arg(cx, tools);
    let recorders = manifest_arg(cx, recorders);
    let agent = cx
        .call_function(
            &Symbol::qualified("agent", "make"),
            Args::new(vec![
                cx.factory().symbol(Symbol::new(":name")).unwrap(),
                cx.factory().symbol(Symbol::new("phase6-agent")).unwrap(),
                cx.factory().symbol(Symbol::new(":runners")).unwrap(),
                runners,
                cx.factory().symbol(Symbol::new(":tools")).unwrap(),
                tools,
                cx.factory().symbol(Symbol::new(":recorders")).unwrap(),
                recorders,
            ]),
        )
        .unwrap();
    cx.call_function(
        &Symbol::qualified("agent", "start"),
        Args::new(vec![agent.clone()]),
    )
    .unwrap();
    agent
}

fn manifest_arg(cx: &mut Cx, mut values: Vec<Value>) -> Value {
    if values.len() == 1 {
        values.remove(0)
    } else {
        cx.factory().list(values).unwrap()
    }
}

fn agent_answer_expr(
    cx: &mut Cx,
    agent: &Value,
    request: Expr,
) -> (Expr, Vec<sim_kernel::Diagnostic>) {
    let frame = agent_answer_frame(cx, agent, request).unwrap();
    let reply = eval_reply_from_frame(cx, &frame).unwrap();
    let expr = reply.value.object().as_expr(cx).unwrap();
    (expr, reply.diagnostics)
}

fn agent_answer_frame(cx: &mut Cx, agent: &Value, request: Expr) -> Result<ServerFrame> {
    let agent = agent.object().downcast_ref::<crate::Agent>().unwrap();
    let frame = request_frame(cx, request);
    agent.site()?.answer(cx, frame)
}

fn fake_runner(cx: &mut Cx, name: &str, script: Vec<Expr>) -> Value {
    let script_value = cx.factory().expr(Expr::List(script)).unwrap();
    cx.call_function(
        &Symbol::qualified("runner", "fake"),
        Args::new(vec![
            cx.factory().symbol(Symbol::new(":name")).unwrap(),
            cx.factory().symbol(Symbol::new(name)).unwrap(),
            cx.factory().symbol(Symbol::new(":model")).unwrap(),
            cx.factory().string(format!("{name}/model")).unwrap(),
            cx.factory().symbol(Symbol::new(":script")).unwrap(),
            script_value,
        ]),
    )
    .unwrap()
}

fn recorder_journal(cx: &mut Cx) -> Value {
    cx.call_function(
        &Symbol::qualified("recorder", "journal"),
        Args::new(Vec::new()),
    )
    .unwrap()
}

fn recorder_snapshot(cx: &mut Cx, recorder: &Value) -> Expr {
    let frame = request_frame(cx, Expr::List(vec![Expr::Symbol(Symbol::new("snapshot"))]));
    let reply = as_component(recorder).answer(cx, frame).unwrap();
    eval_reply_from_frame(cx, &reply)
        .unwrap()
        .value
        .object()
        .as_expr(cx)
        .unwrap()
}

fn model_request(task: &str, extra: Vec<(Expr, Expr)>) -> Expr {
    let mut entries = vec![
        key_expr("model-request", Expr::Bool(true)),
        key_expr("task", Expr::String(task.to_owned())),
        key_expr("messages", Expr::List(Vec::new())),
    ];
    entries.extend(extra);
    Expr::Map(entries)
}

fn final_response(text: &str) -> Expr {
    Expr::Map(vec![
        key_expr("model-response", Expr::Bool(true)),
        key_expr("runner", Expr::Symbol(Symbol::new("phase6-fake"))),
        key_expr("model", Expr::String("runner/fake".to_owned())),
        key_expr(
            "content",
            Expr::List(vec![Expr::Map(vec![
                key_expr("type", Expr::Symbol(Symbol::new("text"))),
                key_expr("text", Expr::String(text.to_owned())),
            ])]),
        ),
        key_expr("stop-reason", Expr::Symbol(Symbol::new("stop"))),
        key_expr("text", Expr::String(text.to_owned())),
    ])
}

fn tool_call_response(tool_calls: Vec<Expr>) -> Expr {
    Expr::Map(vec![
        key_expr("model-response", Expr::Bool(true)),
        key_expr("runner", Expr::Symbol(Symbol::new("phase6-fake"))),
        key_expr("model", Expr::String("runner/fake".to_owned())),
        key_expr("content", Expr::List(Vec::new())),
        key_expr("stop-reason", Expr::Symbol(Symbol::new("tool-call"))),
        key_expr("tool-calls", Expr::List(tool_calls)),
    ])
}

fn tool_call(id: &str, name: Symbol, args: Vec<Expr>) -> Expr {
    Expr::Map(vec![
        key_expr("id", Expr::String(id.to_owned())),
        key_expr("name", Expr::Symbol(name)),
        key_expr("arguments", Expr::List(args)),
    ])
}

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

fn privacy_symbol(policy: &str) -> (Expr, Expr) {
    key_expr("privacy", Expr::Symbol(Symbol::new(policy)))
}

fn key_expr(name: &str, value: Expr) -> (Expr, Expr) {
    (Expr::Symbol(Symbol::new(name)), value)
}