tandem-server 0.6.9

HTTP server for Tandem engine APIs
async fn routine_for_tenant(
    state: &AppState,
    routine_id: &str,
    tenant_context: &TenantContext,
) -> Option<RoutineSpec> {
    state
        .get_routine_for_tenant(routine_id, tenant_context)
        .await
}

async fn routine_run_for_tenant(
    state: &AppState,
    run_id: &str,
    tenant_context: &TenantContext,
) -> Option<RoutineRunRecord> {
    state
        .get_routine_run_for_tenant(run_id, tenant_context)
        .await
}

pub(super) fn objective_from_args(args: &Value, routine_id: &str, entrypoint: &str) -> String {
    args.get("prompt")
        .and_then(|v| v.as_str())
        .map(str::trim)
        .filter(|v| !v.is_empty())
        .map(ToString::to_string)
        .unwrap_or_else(|| {
            format!("Execute automation '{routine_id}' with entrypoint '{entrypoint}'.")
        })
}

pub(super) fn success_criteria_from_args(args: &Value) -> Vec<String> {
    args.get("success_criteria")
        .and_then(|v| v.as_array())
        .map(|rows| {
            rows.iter()
                .filter_map(|row| row.as_str())
                .map(str::trim)
                .filter(|row| !row.is_empty())
                .map(ToString::to_string)
                .collect::<Vec<_>>()
        })
        .unwrap_or_default()
}

pub(super) fn mode_from_args(args: &Value) -> String {
    args.get("mode")
        .and_then(|v| v.as_str())
        .map(str::trim)
        .filter(|v| !v.is_empty())
        .unwrap_or("standalone")
        .to_string()
}

pub(super) fn normalize_automation_mode(raw: Option<&str>) -> Result<String, String> {
    let value = raw.unwrap_or("standalone").trim();
    if value.is_empty() {
        return Ok("standalone".to_string());
    }
    if value.eq_ignore_ascii_case("standalone") {
        return Ok("standalone".to_string());
    }
    if value.eq_ignore_ascii_case("orchestrated") {
        return Ok("orchestrated".to_string());
    }
    Err("mode must be one of standalone|orchestrated".to_string())
}

pub(super) fn validate_model_spec_object(value: &Value, path: &str) -> Result<(), String> {
    let obj = value
        .as_object()
        .ok_or_else(|| format!("{path} must be an object"))?;
    let provider_id = obj
        .get("provider_id")
        .and_then(|v| v.as_str())
        .map(str::trim)
        .filter(|v| !v.is_empty())
        .ok_or_else(|| format!("{path}.provider_id is required"))?;
    let model_id = obj
        .get("model_id")
        .and_then(|v| v.as_str())
        .map(str::trim)
        .filter(|v| !v.is_empty())
        .ok_or_else(|| format!("{path}.model_id is required"))?;
    if provider_id.is_empty() || model_id.is_empty() {
        return Err(format!(
            "{path}.provider_id and {path}.model_id are required"
        ));
    }
    Ok(())
}

pub(crate) fn validate_model_policy(value: &Value) -> Result<(), String> {
    let obj = value
        .as_object()
        .ok_or_else(|| "model_policy must be an object".to_string())?;
    if let Some(default_model) = obj.get("default_model") {
        validate_model_spec_object(default_model, "model_policy.default_model")?;
    }
    if let Some(role_models) = obj.get("role_models") {
        let role_obj = role_models
            .as_object()
            .ok_or_else(|| "model_policy.role_models must be an object".to_string())?;
        for (role, spec) in role_obj {
            validate_model_spec_object(spec, &format!("model_policy.role_models.{role}"))?;
        }
    }
    Ok(())
}