sim-lib-skill 0.1.0-rc.1

SIM workspace package for sim lib skill.
Documentation
use std::{
    collections::BTreeMap,
    sync::{Arc, Mutex},
};

use sim_kernel::{Cx, Error, Expr, Result, Symbol, Value};

use crate::{SkillCacheMode, SkillCard, SkillCassetteMode};

#[derive(Clone, Default)]
pub(crate) struct SkillCallState {
    inner: Arc<Mutex<SkillCallStateInner>>,
}

#[derive(Default)]
struct SkillCallStateInner {
    cache: BTreeMap<String, Expr>,
    cassette: BTreeMap<String, Expr>,
}

impl SkillCallState {
    pub(crate) fn cache_lookup(&self, key: &str) -> Result<Option<Expr>> {
        Ok(self
            .inner
            .lock()
            .map_err(|_| Error::PoisonedLock("skill call state"))?
            .cache
            .get(key)
            .cloned())
    }

    pub(crate) fn cache_store(&self, key: String, value: Expr) -> Result<()> {
        self.inner
            .lock()
            .map_err(|_| Error::PoisonedLock("skill call state"))?
            .cache
            .insert(key, value);
        Ok(())
    }

    pub(crate) fn cassette_lookup(&self, key: &str) -> Result<Option<Expr>> {
        Ok(self
            .inner
            .lock()
            .map_err(|_| Error::PoisonedLock("skill call state"))?
            .cassette
            .get(key)
            .cloned())
    }

    pub(crate) fn cassette_store(&self, key: String, value: Expr) -> Result<()> {
        self.inner
            .lock()
            .map_err(|_| Error::PoisonedLock("skill call state"))?
            .cassette
            .insert(key, value);
        Ok(())
    }
}

#[derive(Clone, Debug)]
pub(crate) struct SkillAuditEntry {
    pub skill_id: String,
    pub transport_kind: String,
    pub outcome: &'static str,
    pub cache: &'static str,
    pub cassette: &'static str,
    pub privacy: String,
    pub capabilities: Vec<String>,
}

impl SkillAuditEntry {
    pub(crate) fn value(&self, cx: &mut Cx) -> Result<Value> {
        let capabilities = cx.factory().list(
            self.capabilities
                .iter()
                .map(|capability| cx.factory().string(capability.clone()))
                .collect::<Result<Vec<_>>>()?,
        )?;
        cx.factory().table(vec![
            (
                Symbol::new("kind"),
                cx.factory()
                    .symbol(Symbol::qualified("skill", "audit-entry"))?,
            ),
            (
                Symbol::new("skill-id"),
                cx.factory().string(self.skill_id.clone())?,
            ),
            (
                Symbol::new("transport-kind"),
                cx.factory().string(self.transport_kind.clone())?,
            ),
            (
                Symbol::new("outcome"),
                cx.factory().symbol(Symbol::new(self.outcome))?,
            ),
            (
                Symbol::new("cache"),
                cx.factory().symbol(Symbol::new(self.cache))?,
            ),
            (
                Symbol::new("cassette"),
                cx.factory().symbol(Symbol::new(self.cassette))?,
            ),
            (
                Symbol::new("privacy"),
                cx.factory().string(self.privacy.clone())?,
            ),
            (Symbol::new("capabilities"), capabilities),
            (
                Symbol::new("input"),
                cx.factory()
                    .symbol(Symbol::qualified("skill", "redacted"))?,
            ),
            (
                Symbol::new("output"),
                cx.factory()
                    .symbol(Symbol::qualified("skill", "redacted"))?,
            ),
        ])
    }
}

pub(crate) fn call_key(cx: &mut Cx, card: &SkillCard, args: &Value) -> Result<String> {
    let args_expr = args.object().as_expr(cx)?;
    let input_shape = card.input_shape.object().as_expr(cx)?;
    let output_shape = card.output_shape.object().as_expr(cx)?;
    let mut capabilities = card
        .capabilities
        .iter()
        .map(|capability| capability.as_str().to_owned())
        .collect::<Vec<_>>();
    capabilities.sort();
    Ok(format!(
        "skill-call-v1;id={};semantic={};privacy={};caps={};input-shape={};output-shape={};args={}",
        card.id,
        card.policy.semantic_key.as_deref().unwrap_or(""),
        card.policy.privacy.as_symbol(),
        capabilities.join(","),
        normalized_expr(&input_shape),
        normalized_expr(&output_shape),
        normalized_expr(&args_expr)
    ))
}

pub(crate) fn value_from_recorded_expr(cx: &mut Cx, expr: Expr) -> Result<Value> {
    match expr {
        Expr::Nil => cx.factory().nil(),
        Expr::Bool(value) => cx.factory().bool(value),
        Expr::Number(number) => cx.factory().number_literal(number.domain, number.canonical),
        Expr::Symbol(symbol) => cx.factory().symbol(symbol),
        Expr::String(text) => cx.factory().string(text),
        Expr::Bytes(bytes) => cx.factory().bytes(bytes),
        Expr::List(items) | Expr::Vector(items) | Expr::Set(items) | Expr::Block(items) => {
            let values = items
                .into_iter()
                .map(|item| value_from_recorded_expr(cx, item))
                .collect::<Result<Vec<_>>>()?;
            cx.factory().list(values)
        }
        Expr::Map(entries) => {
            let mut values = Vec::new();
            for (key, value) in entries {
                let Expr::Symbol(key) = key else {
                    return cx.factory().expr(Expr::Map(vec![(key, value)]));
                };
                values.push((key, value_from_recorded_expr(cx, value)?));
            }
            cx.factory().table(values)
        }
        other => cx.factory().expr(other),
    }
}

pub(crate) fn cache_read_allowed(mode: &SkillCacheMode) -> bool {
    matches!(mode, SkillCacheMode::ReadThrough | SkillCacheMode::ReadOnly)
}

pub(crate) fn cache_write_allowed(mode: &SkillCacheMode) -> bool {
    matches!(
        mode,
        SkillCacheMode::ReadThrough | SkillCacheMode::WriteOnly | SkillCacheMode::Refresh
    )
}

pub(crate) fn cassette_replay_allowed(mode: &SkillCassetteMode) -> bool {
    matches!(
        mode,
        SkillCassetteMode::RecordReplay | SkillCassetteMode::ReplayOnly
    )
}

pub(crate) fn cassette_record_allowed(mode: &SkillCassetteMode) -> bool {
    matches!(
        mode,
        SkillCassetteMode::RecordReplay | SkillCassetteMode::RecordOnly
    )
}

fn normalized_expr(expr: &Expr) -> String {
    match expr {
        Expr::Nil => "nil".to_owned(),
        Expr::Bool(value) => format!("bool:{value}"),
        Expr::Number(number) => format!("number:{}:{}", number.domain, number.canonical),
        Expr::Symbol(symbol) => format!("symbol:{symbol}"),
        Expr::Local(symbol) => format!("local:{symbol}"),
        Expr::String(text) => format!("string:{text:?}"),
        Expr::Bytes(bytes) => format!("bytes:{bytes:?}"),
        Expr::List(items) => normalized_sequence("list", items),
        Expr::Vector(items) => normalized_sequence("vector", items),
        Expr::Set(items) => {
            let mut items = items.iter().map(normalized_expr).collect::<Vec<_>>();
            items.sort();
            format!("set:[{}]", items.join(","))
        }
        Expr::Map(entries) => {
            let mut entries = entries
                .iter()
                .map(|(key, value)| format!("{}=>{}", normalized_expr(key), normalized_expr(value)))
                .collect::<Vec<_>>();
            entries.sort();
            format!("map:{{{}}}", entries.join(","))
        }
        Expr::Call { operator, args } => {
            format!(
                "call:{}({})",
                normalized_expr(operator),
                args.iter()
                    .map(normalized_expr)
                    .collect::<Vec<_>>()
                    .join(",")
            )
        }
        Expr::Infix {
            operator,
            left,
            right,
        } => format!(
            "infix:{}:{}:{}",
            operator,
            normalized_expr(left),
            normalized_expr(right)
        ),
        Expr::Prefix { operator, arg } => format!("prefix:{operator}:{}", normalized_expr(arg)),
        Expr::Postfix { operator, arg } => {
            format!("postfix:{operator}:{}", normalized_expr(arg))
        }
        Expr::Quote { mode, expr } => format!("quote:{mode:?}:{}", normalized_expr(expr)),
        Expr::Annotated { expr, annotations } => {
            let mut annotations = annotations
                .iter()
                .map(|(key, value)| format!("{key}:{}", normalized_expr(value)))
                .collect::<Vec<_>>();
            annotations.sort();
            format!(
                "annotated:{}:[{}]",
                normalized_expr(expr),
                annotations.join(",")
            )
        }
        Expr::Block(items) => normalized_sequence("block", items),
        Expr::Extension { tag, payload } => {
            format!("extension:{tag}:{}", normalized_expr(payload))
        }
    }
}

fn normalized_sequence(kind: &str, items: &[Expr]) -> String {
    format!(
        "{kind}:[{}]",
        items
            .iter()
            .map(normalized_expr)
            .collect::<Vec<_>>()
            .join(",")
    )
}