sim-lib-agent 0.1.2

Agent runtime surfaces for SIM.
Documentation
use std::{
    collections::VecDeque,
    sync::{
        Arc, Mutex,
        atomic::{AtomicUsize, Ordering},
    },
};

use super::{
    AgentMission, AtelierAgentRole, GuardCapability, HumanDecisionPoint, MissionRun, RadarChunk,
    RadarIndex, SourceSpan, WorkspaceLease, WorkspaceLeaseMode, detect_workspace_lease_conflicts,
    run_mission_handoff,
};
use crate::{ModelCard, ModelRequest, ModelResponse, ModelRunner};
use sim_codec::{Input, decode_with_codec, encode_with_codec};
use sim_codec_binary::BinaryCodecLib;
use sim_codec_json::JsonCodecLib;
use sim_codec_lisp::LispCodecLib;
use sim_kernel::{
    Cx, DefaultFactory, EagerPolicy, EncodeOptions, Expr, ReadPolicy, Result, Symbol,
};

struct ScriptedRunner {
    script: Mutex<VecDeque<Vec<Expr>>>,
    calls: AtomicUsize,
}

impl ScriptedRunner {
    fn new(script: impl IntoIterator<Item = Vec<Expr>>) -> Self {
        Self {
            script: Mutex::new(script.into_iter().collect()),
            calls: AtomicUsize::new(0),
        }
    }

    fn calls(&self) -> usize {
        self.calls.load(Ordering::SeqCst)
    }
}

impl ModelRunner for ScriptedRunner {
    fn card(&self) -> ModelCard {
        ModelCard::new(
            Symbol::new("runner/mission-script"),
            "mission/script",
            Symbol::new("test"),
            Symbol::new("local"),
        )
    }

    fn infer(&self, _cx: &mut sim_kernel::Cx, _request: ModelRequest) -> Result<ModelResponse> {
        self.calls.fetch_add(1, Ordering::SeqCst);
        let content = self
            .script
            .lock()
            .unwrap()
            .pop_front()
            .expect("scripted mission runner exhausted");
        Ok(ModelResponse::new(
            Symbol::new("runner/mission-script"),
            "mission/script",
            content,
            Symbol::new("stop"),
        ))
    }
}

#[test]
fn mission_descriptor_uses_sup56_slots_and_round_trips() {
    let mut cx = eval_cx();
    install_roundtrip_codecs(&mut cx);
    let mission = mission_fixture();
    let descriptor = mission.descriptor();

    assert_eq!(descriptor.id, Symbol::qualified("agent/mission", "sup2-10"));
    assert_eq!(descriptor.memory[0].name, Symbol::new("f2-radar-retrieve"));
    assert_eq!(descriptor.plan[0].name, Symbol::new("f3-decompose"));
    assert_eq!(descriptor.evaluation[0].name, Symbol::new("f3-reflect"));
    assert_eq!(descriptor.trace[0].name, Symbol::new("dev-cassette-ledger"));
    assert!(
        descriptor
            .act
            .iter()
            .any(|slot| slot.name == AtelierAgentRole::HumanGate.as_symbol())
    );

    let expr = descriptor.as_expr();
    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 workspace_lease_conflicts_detect_overlapping_write_claims() {
    let left = AgentMission::new(Symbol::qualified("agent/mission", "left"), "sim-agent-net")
        .with_lease(WorkspaceLease::file(
            "sim-agent-net",
            "crates/sim-lib-agent/src/atelier/mission.rs",
        ));
    let right =
        AgentMission::new(Symbol::qualified("agent/mission", "right"), "sim-agent-net").with_lease(
            WorkspaceLease::directory("sim-agent-net", "crates/sim-lib-agent/src/atelier"),
        );
    let reader = AgentMission::new(
        Symbol::qualified("agent/mission", "reader"),
        "sim-agent-net",
    )
    .with_lease(
        WorkspaceLease::file(
            "sim-agent-net",
            "crates/sim-lib-agent/src/atelier/mission.rs",
        )
        .for_read(),
    );

    let conflicts = detect_workspace_lease_conflicts(&[left.clone(), right.clone()]);
    assert_eq!(conflicts.len(), 1);
    assert_eq!(conflicts[0].left_mission, *left.id());
    assert_eq!(conflicts[0].right_mission, *right.id());

    let shared_reads = detect_workspace_lease_conflicts(&[
        reader.clone(),
        reader
            .clone()
            .with_evidence_stream(Symbol::qualified("atelier/dev", "reader-two")),
    ]);
    assert!(shared_reads.is_empty());
    assert_eq!(reader.leases()[0].mode, WorkspaceLeaseMode::SharedRead);
}

#[test]
fn fake_runner_handoff_records_dev_cassette_without_network() {
    let runner = ScriptedRunner::new([
        vec![Expr::List(vec![
            task_expr("retrieve", "Retrieve ranked context"),
            task_expr("edit", "Apply scoped edit"),
        ])],
        vec![Expr::String("retrieved context".to_owned())],
        vec![Expr::String("edited mission code".to_owned())],
        vec![Expr::Map(vec![
            (Expr::Symbol(Symbol::new("accept")), Expr::Bool(true)),
            (
                Expr::Symbol(Symbol::new("critique")),
                Expr::String("evidence matches leases".to_owned()),
            ),
        ])],
    ]);
    let mut cx = eval_cx();

    let report =
        run_mission_handoff(&mut cx, &mission_fixture(), &radar_fixture(), &runner, 3).unwrap();

    assert_eq!(runner.calls(), 4);
    assert_eq!(report.decomposition.subtasks.len(), 2);
    assert!(report.reflection.accept);
    assert!(report.confidence > 0.5);
    assert_eq!(
        report.evidence.cassette().envelopes().len(),
        9,
        "retrieve, plan, guard, two handoffs, validation, docs, human gate, reflect"
    );
    assert_eq!(
        report.evidence.content_hash(),
        report.evidence.replay_content_hash().unwrap()
    );
}

fn mission_fixture() -> AgentMission {
    AgentMission::new(
        Symbol::qualified("agent/mission", "sup2-10"),
        "sim-agent-net",
    )
    .with_goal("Implement the Atelier mission model")
    .with_scope_summary("mission data and deterministic handoff in sim-lib-agent")
    .with_capability(GuardCapability::EditRepo("sim-agent-net".to_owned()))
    .with_capability(GuardCapability::RunValidation("sim-agent-net".to_owned()))
    .with_lease(WorkspaceLease::crate_name("sim-agent-net", "sim-lib-agent"))
    .with_validation(MissionRun::new(
        "agent-tests",
        "cargo test -p sim-lib-agent mission",
    ))
    .with_docs_run(MissionRun::new(
        "simdoc",
        "cargo run -p xtask -- simdoc --check",
    ))
    .with_decision_point(HumanDecisionPoint::new(
        "push",
        "Confirm commits and pins before push",
    ))
    .with_recipe_pattern(Symbol::new("a30-009-agentic-workflow"))
}

fn radar_fixture() -> RadarIndex {
    let mut chunk = RadarChunk::new(
        "mission-contract",
        "Mission contract",
        SourceSpan {
            repo: "sim-agent-net".to_owned(),
            path: "crates/sim-lib-agent/src/atelier/mission.rs".to_owned(),
            line: 1,
        },
        "rustdoc",
        "Cartographer retrieves mission leases and agent pattern descriptors",
    );
    chunk.agent_roles = vec!["cartographer".to_owned()];
    RadarIndex {
        chunks: vec![chunk],
    }
}

fn task_expr(id: &str, prompt: &str) -> Expr {
    Expr::Map(vec![
        (Expr::Symbol(Symbol::new("id")), Expr::String(id.to_owned())),
        (
            Expr::Symbol(Symbol::new("prompt")),
            Expr::String(prompt.to_owned()),
        ),
    ])
}

fn eval_cx() -> Cx {
    Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory))
}

fn install_roundtrip_codecs(cx: &mut Cx) {
    let binary = BinaryCodecLib::new(cx.registry_mut().fresh_codec_id());
    cx.load_lib(&binary).unwrap();
    let json = JsonCodecLib::new(cx.registry_mut().fresh_codec_id());
    cx.load_lib(&json).unwrap();
    let lisp = LispCodecLib::new(cx.registry_mut().fresh_codec_id()).unwrap();
    cx.load_lib(&lisp).unwrap();
}