lash-core 0.1.0-alpha.37

Sans-IO turn machine and runtime kernel for the lash agent runtime.
Documentation
use crate::plugin::PluginError;

use super::events::{
    ProcessAwaitOutput, ProcessEventSemantics, ProcessEventSemanticsSpec, ProcessTerminalSemantics,
    ProcessTerminalSpec, ProcessTerminalState, ProcessValueSelector, ProcessWake,
    ProcessWakeDedupeKey, ProcessWakeSpec,
};

pub fn materialize_process_event_semantics(
    process_id: &str,
    sequence: u64,
    payload: &serde_json::Value,
    spec: &ProcessEventSemanticsSpec,
) -> Result<ProcessEventSemantics, PluginError> {
    materialize_event_semantics(process_id, sequence, payload, spec)
}

pub(super) fn materialize_event_semantics(
    process_id: &str,
    sequence: u64,
    payload: &serde_json::Value,
    spec: &ProcessEventSemanticsSpec,
) -> Result<ProcessEventSemantics, PluginError> {
    let terminal = spec
        .terminal
        .as_ref()
        .map(|terminal| materialize_terminal_semantics(payload, terminal))
        .transpose()?;
    let wake = spec
        .wake
        .as_ref()
        .map(|wake| materialize_wake(process_id, sequence, payload, wake))
        .transpose()?
        .flatten();
    Ok(ProcessEventSemantics { terminal, wake })
}

fn materialize_terminal_semantics(
    payload: &serde_json::Value,
    terminal: &ProcessTerminalSpec,
) -> Result<ProcessTerminalSemantics, PluginError> {
    let await_output = match &terminal.await_output {
        Some(selector) => {
            let selected = select_value(payload, selector)?;
            serde_json::from_value::<ProcessAwaitOutput>(selected.clone())
                .unwrap_or_else(|_| selected_value_to_await_output(terminal.state, selected))
        }
        None if terminal.state == ProcessTerminalState::Completed => ProcessAwaitOutput::Success {
            value: payload.clone(),
            control: None,
        },
        None => {
            return Err(PluginError::Session(
                "failed or cancelled terminal events must declare await output".to_string(),
            ));
        }
    };
    Ok(ProcessTerminalSemantics {
        state: terminal.state,
        await_output,
    })
}

fn selected_value_to_await_output(
    state: ProcessTerminalState,
    value: serde_json::Value,
) -> ProcessAwaitOutput {
    match state {
        ProcessTerminalState::Completed => ProcessAwaitOutput::Success {
            value,
            control: None,
        },
        ProcessTerminalState::Failed => ProcessAwaitOutput::Failure {
            class: crate::ToolFailureClass::Execution,
            code: "process_failed".to_string(),
            message: selector_value_to_string(&value),
            raw: Some(value),
            control: None,
        },
        ProcessTerminalState::Cancelled => ProcessAwaitOutput::Cancelled {
            message: selector_value_to_string(&value),
            raw: Some(value),
            control: None,
        },
    }
}

fn materialize_wake(
    process_id: &str,
    sequence: u64,
    payload: &serde_json::Value,
    wake: &ProcessWakeSpec,
) -> Result<Option<ProcessWake>, PluginError> {
    if let Some(when) = &wake.when {
        let selected = select_value(payload, when)?;
        if !selector_value_is_truthy(&selected) {
            return Ok(None);
        }
    }
    let input = selector_value_to_string(&select_value(payload, &wake.input)?);
    let dedupe_key = match &wake.dedupe_key {
        ProcessWakeDedupeKey::EventIdentity => format!("{process_id}:{sequence}"),
        ProcessWakeDedupeKey::Selector(selector) => {
            selector_value_to_string(&select_value(payload, selector)?)
        }
        ProcessWakeDedupeKey::Const(value) => value.clone(),
    };
    Ok(Some(ProcessWake { input, dedupe_key }))
}

pub(super) fn select_value(
    payload: &serde_json::Value,
    selector: &ProcessValueSelector,
) -> Result<serde_json::Value, PluginError> {
    match selector {
        ProcessValueSelector::Payload => Ok(payload.clone()),
        ProcessValueSelector::Pointer(pointer) => {
            payload.pointer(pointer).cloned().ok_or_else(|| {
                PluginError::Session(format!("payload pointer `{pointer}` did not match"))
            })
        }
        ProcessValueSelector::Const(value) => Ok(value.clone()),
        ProcessValueSelector::Template { template, fields } => {
            let mut rendered = template.clone();
            for (name, selector) in fields {
                let value = select_value(payload, selector)?;
                rendered =
                    rendered.replace(&format!("{{{name}}}"), &selector_value_to_string(&value));
            }
            Ok(serde_json::Value::String(rendered))
        }
        ProcessValueSelector::Present(pointer) => {
            Ok(serde_json::Value::Bool(payload.pointer(pointer).is_some()))
        }
    }
}

fn selector_value_to_string(value: &serde_json::Value) -> String {
    value
        .as_str()
        .map(ToOwned::to_owned)
        .unwrap_or_else(|| value.to_string())
}

fn selector_value_is_truthy(value: &serde_json::Value) -> bool {
    match value {
        serde_json::Value::Null => false,
        serde_json::Value::Bool(value) => *value,
        serde_json::Value::String(value) => !value.is_empty(),
        serde_json::Value::Array(value) => !value.is_empty(),
        serde_json::Value::Object(value) => !value.is_empty(),
        serde_json::Value::Number(_) => true,
    }
}