sim-lib-agent 0.1.6

Agent runtime surfaces for SIM.
Documentation
use crate::{stringish_from_value, value_from_expr};
use sim_codec::{Input, decode_with_codec};
use sim_cookbook::{Expectation, parse_recipe};
use sim_kernel::{Args, Cx, Error, ReadPolicy, Result, Symbol, Value};
use std::str;

pub(crate) fn agent_recipe_result_value(cx: &mut Cx, args: Args) -> Result<Value> {
    let recipe_id = match args.into_vec().as_slice() {
        [recipe_id] => stringish_from_value(
            cx,
            recipe_id.clone(),
            "agent/recipe-result expects a recipe id",
        )?,
        _ => {
            return Err(Error::Eval(
                "agent/recipe-result expects one recipe id".to_owned(),
            ));
        }
    };

    let mut matching_recipes = Vec::new();
    for (path, bytes) in crate::RECIPES
        .iter()
        .filter(|(path, _)| path.ends_with("/recipe.toml"))
    {
        let recipe = recipe_from_embedded(path, bytes).map_err(Error::Eval)?;
        if recipe_id_matches(&recipe.full_id, &recipe.manifest.id, &recipe_id) {
            matching_recipes.push(recipe);
        }
    }
    let recipe = match matching_recipes.as_slice() {
        [recipe] => recipe,
        [] => {
            return Err(Error::Eval(format!(
                "agent/recipe-result could not find recipe {recipe_id}"
            )));
        }
        _ => {
            return Err(Error::Eval(format!(
                "agent/recipe-result matched multiple recipes for {recipe_id}"
            )));
        }
    };

    let expectation = match recipe.manifest.expect.as_slice() {
        [expectation] if expectation.form == 0 => expectation,
        [] => {
            return Err(Error::Eval(format!(
                "agent/recipe-result recipe {} has no expected result",
                recipe.full_id
            )));
        }
        [expectation] => {
            return Err(Error::Eval(format!(
                "agent/recipe-result recipe {} expects form {}, not form 0",
                recipe.full_id, expectation.form
            )));
        }
        _ => {
            return Err(Error::Eval(format!(
                "agent/recipe-result recipe {} has multiple expected results",
                recipe.full_id
            )));
        }
    };

    let expr = decode_expectation(cx, &recipe.manifest.codec, expectation)?;
    value_from_expr(cx, &expr)
}

struct EmbeddedRecipe {
    full_id: String,
    manifest: sim_cookbook::RecipeManifest,
}

fn recipe_from_embedded(path: &str, bytes: &[u8]) -> std::result::Result<EmbeddedRecipe, String> {
    let text = str::from_utf8(bytes).map_err(|err| format!("{path}: {err}"))?;
    let manifest = parse_recipe(text).map_err(|err| format!("{path}: {err}"))?;
    let chapter = path
        .split('/')
        .next()
        .ok_or_else(|| format!("{path}: missing chapter"))?;
    Ok(EmbeddedRecipe {
        full_id: format!("agent/{chapter}/{}", manifest.id),
        manifest,
    })
}

fn recipe_id_matches(full_id: &str, local_id: &str, requested: &str) -> bool {
    full_id == requested || local_id == requested
}

fn decode_expectation(
    cx: &mut Cx,
    codec: &str,
    expectation: &Expectation,
) -> Result<sim_kernel::Expr> {
    let codec = Symbol::qualified("codec", codec);
    decode_with_codec(
        cx,
        &codec,
        Input::Text(expectation.result.clone()),
        ReadPolicy::default(),
    )
}