sim-lib-agent 0.1.3

Agent runtime surfaces for SIM.
Documentation
use super::model::{agent_from_value, resolve_agent_address};
use crate::installed_codecs;
use sim_kernel::{CapabilityName, Cx, Error, Expr, Result, Symbol};
use sim_lib_server::{Connection, LineDriver, ServerAddress};

const AGENT_DRIVE_CAPABILITY: &str = "agent-drive";

pub(crate) fn agent_line_driver_factory(
    cx: &mut Cx,
    spec: &Expr,
) -> Result<Option<Box<dyn LineDriver>>> {
    cx.require(&CapabilityName::new(AGENT_DRIVE_CAPABILITY))?;
    let target = driver_agent_target(spec)?;
    if let Ok(value) = cx.eval_expr(target.clone())
        && let Ok(agent) = agent_from_value(&value)
    {
        let state = agent
            .state
            .lock()
            .map_err(|_| Error::PoisonedLock("agent state"))?;
        let default_codec = state.default_codec.clone();
        let supported_codecs = state.supported_codecs.clone();
        drop(state);
        let connection = Connection::with_session(
            ServerAddress::Local,
            default_codec,
            supported_codecs,
            agent.site()?,
            None,
            agent.policy.clone(),
        )?;
        return Ok(Some(Box::new(AgentLineDriver {
            connection,
            output: String::new(),
        })));
    }
    let name = match target {
        Expr::Symbol(symbol) => symbol,
        _ => {
            return Err(Error::Eval(
                "agent driver expects (agent <symbol>)".to_owned(),
            ));
        }
    };
    let address = ServerAddress::Agent {
        agent: name.to_string(),
    };
    let Some(resolved) = resolve_agent_address(cx, &address, &installed_codecs(cx))? else {
        return Err(Error::Eval(format!(
            "agent driver target {} is not started",
            name
        )));
    };
    let connection = Connection::with_session(
        address,
        resolved.selected_codec,
        resolved.supported_codecs,
        resolved.site,
        None,
        sim_lib_server::IsolationPolicy::default(),
    )?;
    Ok(Some(Box::new(AgentLineDriver {
        connection,
        output: String::new(),
    })))
}

struct AgentLineDriver {
    connection: Connection,
    output: String,
}

impl LineDriver for AgentLineDriver {
    fn read_line(&mut self, cx: &mut Cx, prompt: &str) -> Result<Option<String>> {
        let request = Expr::Map(vec![
            (
                Expr::Symbol(Symbol::new("driver")),
                Expr::Symbol(Symbol::new("agent")),
            ),
            (
                Expr::Symbol(Symbol::new("prompt")),
                Expr::String(prompt.to_owned()),
            ),
            (
                Expr::Symbol(Symbol::new("output")),
                Expr::String(self.output.clone()),
            ),
        ]);
        let reply = self.connection.request(cx, request, None, Vec::new())?;
        match reply.object().as_expr(cx)? {
            Expr::Nil => Ok(None),
            Expr::String(text) => Ok(Some(text)),
            Expr::Map(entries) => {
                if entries.iter().any(|(key, value)| {
                    matches!(key, Expr::Symbol(symbol) if symbol.name.as_ref() == "done")
                        && matches!(value, Expr::Bool(true))
                }) {
                    return Ok(None);
                }
                let line = entries.iter().find_map(|(key, value)| match (key, value) {
                    (Expr::Symbol(symbol), Expr::String(text))
                        if symbol.name.as_ref() == "line" =>
                    {
                        Some(text.clone())
                    }
                    _ => None,
                });
                Ok(line)
            }
            other => Err(Error::Eval(format!(
                "agent driver expected nil, string, or {{line ...}}, found {other:?}"
            ))),
        }
    }

    fn write_output(&mut self, _cx: &mut Cx, output: &str) -> Result<()> {
        self.output.push_str(output);
        Ok(())
    }
}

fn driver_agent_target(spec: &Expr) -> Result<Expr> {
    match spec {
        Expr::List(items) | Expr::Vector(items) => match items.get(1) {
            Some(expr) => Ok(expr.clone()),
            _ => Err(Error::Eval(
                "agent driver expects (agent <symbol>)".to_owned(),
            )),
        },
        Expr::Symbol(symbol) if symbol.name.as_ref() == "agent" => Err(Error::Eval(
            "agent driver requires an agent name".to_owned(),
        )),
        _ => Err(Error::Eval(
            "agent driver expects (agent <symbol>)".to_owned(),
        )),
    }
}