sim-lib-agent 0.1.1

Agent runtime surfaces for SIM.
Documentation
use super::super::{
    model::{AgentComponent, ComponentBackend, RunnerBackend, component_value},
    options::{parse_component_options, string_option, symbol_option},
};
use crate::{
    AI_RUNNER_CAPABILITY, AI_RUNNER_LOCAL_CAPABILITY, ComponentKind, util::installed_codecs,
};
use sim_kernel::{Args, CapabilityName, Cx, Error, Expr, Result, Symbol, Value};
use sim_lib_agent_runner_process::{ProcessProtocol, ProcessRunner};
use sim_lib_server::{ServerAddress, parse_duration};
use std::{sync::Arc, time::Duration};

pub(crate) fn runner_process_value(cx: &mut Cx, args: Args) -> Result<Value> {
    let options = parse_component_options(cx, args, "runner/process")?;
    let symbol = symbol_option(cx, &options, "name", Symbol::qualified("runner", "process"))?;
    let model = string_option(cx, &options, "model", "runner/process")?;
    let command = options
        .get("command")
        .ok_or_else(|| Error::Eval("runner/process :command expects a string or symbol".to_owned()))
        .and_then(|value| {
            crate::util::stringish_from_value(
                cx,
                value.clone(),
                "runner/process :command expects a string or symbol",
            )
        })?;
    let protocol = protocol_option(cx, &options)?;
    let timeout = timeout_option(cx, &options)?;
    let max_output_bytes = max_output_bytes_option(cx, &options)?;
    component_value(
        cx,
        AgentComponent {
            symbol: symbol.clone(),
            kind: ComponentKind::Runner,
            capabilities: vec![
                CapabilityName::new(AI_RUNNER_CAPABILITY),
                CapabilityName::new(AI_RUNNER_LOCAL_CAPABILITY),
            ],
            address: ServerAddress::Local,
            codecs: installed_codecs(cx),
            spec: runner_spec(&model, &command, protocol, timeout, max_output_bytes),
            backend: ComponentBackend::Runner(RunnerBackend::External {
                runner: Arc::new(ProcessRunner::new(
                    symbol,
                    model,
                    command,
                    protocol,
                    timeout,
                    max_output_bytes,
                )),
            }),
        },
    )
}

fn runner_spec(
    model: &str,
    command: &str,
    protocol: ProcessProtocol,
    timeout: Duration,
    max_output_bytes: usize,
) -> Vec<(Symbol, Expr)> {
    vec![
        (Symbol::new("backend"), Expr::Symbol(Symbol::new("process"))),
        (Symbol::new("model"), Expr::String(model.to_owned())),
        (Symbol::new("command"), Expr::String(command.to_owned())),
        (
            Symbol::new("protocol"),
            Expr::Symbol(Symbol::new(match protocol {
                ProcessProtocol::JsonStdio => "json-stdio",
                ProcessProtocol::LineText => "line-text",
            })),
        ),
        (
            Symbol::new("timeout"),
            Expr::String(format!("{}ms", timeout.as_millis())),
        ),
        (
            Symbol::new("max-output-bytes"),
            Expr::Number(sim_kernel::NumberLiteral {
                domain: Symbol::qualified("numbers", "f64"),
                canonical: max_output_bytes.to_string(),
            }),
        ),
    ]
}

fn protocol_option(
    cx: &mut Cx,
    options: &std::collections::HashMap<String, Value>,
) -> Result<ProcessProtocol> {
    let protocol = options
        .get("protocol")
        .map(|value| {
            crate::util::stringish_from_value(
                cx,
                value.clone(),
                "runner/process :protocol expects a string or symbol",
            )
        })
        .transpose()?
        .unwrap_or_else(|| "json-stdio".to_owned());
    match protocol.as_str() {
        "json-stdio" => Ok(ProcessProtocol::JsonStdio),
        "line-text" => Ok(ProcessProtocol::LineText),
        _ => Err(Error::Eval(
            "runner/process :protocol expects json-stdio or line-text".to_owned(),
        )),
    }
}

fn timeout_option(
    cx: &mut Cx,
    options: &std::collections::HashMap<String, Value>,
) -> Result<Duration> {
    match options.get("timeout") {
        Some(value) => parse_duration(&value.object().as_expr(cx)?),
        None => Ok(Duration::from_secs(30)),
    }
}

fn max_output_bytes_option(
    cx: &mut Cx,
    options: &std::collections::HashMap<String, Value>,
) -> Result<usize> {
    match options.get("max-output-bytes") {
        Some(value) => match value.object().as_expr(cx)? {
            Expr::String(text) => parse_max_output_bytes(&text),
            Expr::Number(number) => parse_max_output_bytes(&number.canonical),
            _ => Err(max_output_bytes_error()),
        },
        None => Ok(1024 * 1024),
    }
}

fn parse_max_output_bytes(text: &str) -> Result<usize> {
    text.parse::<usize>().map_err(|_| max_output_bytes_error())
}

fn max_output_bytes_error() -> Error {
    Error::Eval("runner/process :max-output-bytes expects an integer".to_owned())
}