sim-lib-agent 0.1.1

Agent runtime surfaces for SIM.
Documentation
use super::shared::values_option;
use super::topology_runtime::{
    build_debate_connection, build_market_connection, build_mesh_connection, build_ring_connection,
    build_speculate_verify_connection, build_star_connection, pipeline_connection,
};
use crate::{maybe_u32_option, parse_component_options, symbol_from_value, symbol_option};
use sim_kernel::{Args, Cx, Error, Result, Symbol, Value};
use sim_lib_server::{Connection, EvalSite, LocalEvalSite, ServerAddress};
use std::sync::Arc;

use super::super::first_codec;

pub(crate) fn topology_ring_value(cx: &mut Cx, args: Args) -> Result<Value> {
    let options = parse_component_options(cx, args, "topology/ring")?;
    let agents = values_option(cx, &options, "agents")?;
    let role_cycle = role_cycle_option(cx, &options)?;
    let max_turns = maybe_u32_option(cx, &options, "max-turns")?
        .unwrap_or_else(|| u32::try_from(agents.len().max(1)).unwrap_or(u32::MAX));
    let connection = build_ring_connection(cx, agents, role_cycle, max_turns)?;
    cx.factory().opaque(connection)
}

pub(crate) fn topology_star_value(cx: &mut Cx, args: Args) -> Result<Value> {
    let options = parse_component_options(cx, args, "topology/star")?;
    let hub = options
        .get("hub")
        .cloned()
        .ok_or_else(|| Error::Eval("topology/star requires :hub".to_owned()))?;
    let spokes = values_option(cx, &options, "spokes")?;
    let hub_role = symbol_option(cx, &options, "hub-role", Symbol::new("planner"))?;
    let spoke_role = symbol_option(cx, &options, "spoke-role", Symbol::new("worker"))?;
    let connection = build_star_connection(cx, hub, spokes, hub_role, spoke_role)?;
    cx.factory().opaque(connection)
}

pub(crate) fn topology_mesh_value(cx: &mut Cx, args: Args) -> Result<Value> {
    let options = parse_component_options(cx, args, "topology/mesh")?;
    let agents = values_option(cx, &options, "agents")?;
    let judge = options
        .get("judge")
        .cloned()
        .ok_or_else(|| Error::Eval("topology/mesh requires :judge".to_owned()))?;
    let max_rounds = maybe_u32_option(cx, &options, "max-rounds")?.unwrap_or(2);
    let connection = build_mesh_connection(cx, agents, judge, max_rounds)?;
    cx.factory().opaque(connection)
}

pub(crate) fn topology_market_value(cx: &mut Cx, args: Args) -> Result<Value> {
    let options = parse_component_options(cx, args, "topology/market")?;
    let workers = values_option(cx, &options, "workers")?;
    let router = options
        .get("router")
        .cloned()
        .ok_or_else(|| Error::Eval("topology/market requires :router".to_owned()))?;
    let connection = build_market_connection(cx, workers, router)?;
    cx.factory().opaque(connection)
}

pub(crate) fn topology_debate_value(cx: &mut Cx, args: Args) -> Result<Value> {
    let options = parse_component_options(cx, args, "topology/debate")?;
    let pro = options
        .get("pro")
        .cloned()
        .ok_or_else(|| Error::Eval("topology/debate requires :pro".to_owned()))?;
    let con = options
        .get("con")
        .cloned()
        .ok_or_else(|| Error::Eval("topology/debate requires :con".to_owned()))?;
    let judge = options
        .get("judge")
        .cloned()
        .ok_or_else(|| Error::Eval("topology/debate requires :judge".to_owned()))?;
    let rounds = maybe_u32_option(cx, &options, "rounds")?.unwrap_or(1);
    let connection = build_debate_connection(cx, pro, con, judge, rounds)?;
    cx.factory().opaque(connection)
}

pub(crate) fn topology_speculate_verify_value(cx: &mut Cx, args: Args) -> Result<Value> {
    let options = parse_component_options(cx, args, "topology/speculate-verify")?;
    let speculator = options
        .get("speculator")
        .cloned()
        .ok_or_else(|| Error::Eval("topology/speculate-verify requires :speculator".to_owned()))?;
    let verifier = options
        .get("verifier")
        .cloned()
        .ok_or_else(|| Error::Eval("topology/speculate-verify requires :verifier".to_owned()))?;
    let on_mismatch = mismatch_policy(cx, &options)?;
    let connection = build_speculate_verify_connection(cx, speculator, verifier, on_mismatch)?;
    cx.factory().opaque(connection)
}

pub(crate) fn topology_open_claw_value(cx: &mut Cx, args: Args) -> Result<Value> {
    let options = parse_component_options(cx, args, "topology/open-claw")?;
    let steps = topology_steps(cx, &options)?;
    if steps.is_empty() {
        return Err(Error::Eval(
            "topology/open-claw requires at least one step".to_owned(),
        ));
    }
    let connection = pipeline_connection(
        cx,
        steps
            .into_iter()
            .map(super::shared::agent_connection_for_value)
            .collect::<Result<Vec<_>>>()?,
    )?;
    cx.factory().opaque(connection)
}

pub(crate) fn gateway_create_value(cx: &mut Cx, args: Args) -> Result<Value> {
    let options = parse_component_options(cx, args, "gateway/create")?;
    let address = match options.get("address") {
        Some(value) => ServerAddress::from_expr(&value.object().as_expr(cx)?)?,
        None => ServerAddress::Local,
    };
    let codecs = crate::installed_codecs(cx);
    let site: Arc<dyn EvalSite> = Arc::new(LocalEvalSite::new(address.clone(), codecs.clone()));
    let connection = Connection::with_session(
        address,
        first_codec(&codecs),
        codecs,
        site,
        None,
        sim_lib_server::IsolationPolicy::default(),
    )?;
    cx.factory().opaque(Arc::new(connection))
}

fn topology_steps(
    cx: &mut Cx,
    options: &std::collections::HashMap<String, Value>,
) -> Result<Vec<Value>> {
    let mut steps = values_option(cx, options, "steps")?;
    steps.extend(values_option(cx, options, "agents")?);
    if let Some(hub) = options.get("hub") {
        steps.push(hub.clone());
    }
    steps.extend(values_option(cx, options, "spokes")?);
    if let Some(pro) = options.get("pro") {
        steps.push(pro.clone());
    }
    if let Some(con) = options.get("con") {
        steps.push(con.clone());
    }
    if let Some(judge) = options.get("judge") {
        steps.push(judge.clone());
    }
    if let Some(speculator) = options.get("speculator") {
        steps.push(speculator.clone());
    }
    if let Some(verifier) = options.get("verifier") {
        steps.push(verifier.clone());
    }
    if let Some(router) = options.get("router") {
        steps.push(router.clone());
    }
    Ok(steps)
}

fn role_cycle_option(
    cx: &mut Cx,
    options: &std::collections::HashMap<String, Value>,
) -> Result<Vec<Symbol>> {
    let Some(value) = options.get("role-cycle") else {
        return Ok(vec![
            Symbol::new("planner"),
            Symbol::new("worker"),
            Symbol::new("critic"),
        ]);
    };
    match value.object().as_expr(cx)? {
        sim_kernel::Expr::List(items) | sim_kernel::Expr::Vector(items) => items
            .into_iter()
            .map(|expr| match expr {
                sim_kernel::Expr::Symbol(symbol) => Ok(symbol),
                other => Err(Error::Eval(format!(
                    "topology/ring :role-cycle expects symbols, found {other:?}"
                ))),
            })
            .collect(),
        sim_kernel::Expr::Symbol(symbol) => Ok(vec![symbol]),
        other => Err(Error::Eval(format!(
            "topology/ring :role-cycle expects a symbol list, found {other:?}"
        ))),
    }
}

fn mismatch_policy(
    cx: &mut Cx,
    options: &std::collections::HashMap<String, Value>,
) -> Result<Symbol> {
    match options.get("on-mismatch") {
        Some(value) => symbol_from_value(
            cx,
            value.clone(),
            "topology/speculate-verify :on-mismatch expects a symbol",
        ),
        None => Ok(Symbol::new("retry")),
    }
}