smol-workflow-engine 0.2.1

Rust implementation of the smol-workflows engine.
Documentation
use super::types::{
    AgentProvider, AgentProviderResult, AgentProviderRunInput, AgentProviderSchemaMode,
    AgentProviderUsageMode, AgentUsage, AgentUsageCost,
};
use serde_json::{Map, Value};

#[derive(Debug, Default, Clone, Copy)]
pub struct DebugAgentProvider;

impl DebugAgentProvider {
    pub fn new() -> Self {
        Self
    }
}

#[async_trait::async_trait]
impl AgentProvider for DebugAgentProvider {
    fn name(&self) -> &str {
        "debug"
    }

    fn schema_mode(&self) -> AgentProviderSchemaMode {
        AgentProviderSchemaMode::Builtin
    }

    fn usage_mode(&self) -> AgentProviderUsageMode {
        AgentProviderUsageMode::Builtin
    }

    async fn run(&self, input: AgentProviderRunInput) -> anyhow::Result<AgentProviderResult> {
        log::debug!(
            "running debug provider phase={:?} prompt_len={} schema={}",
            input.context.phase.as_deref(),
            input.prompt.len(),
            input
                .options
                .as_ref()
                .and_then(|options| options.get("schema"))
                .is_some()
        );
        let output = input
            .options
            .as_ref()
            .and_then(|options| options.get("schema"))
            .map(generate_debug_value_from_schema)
            .unwrap_or_else(|| Value::String(format!("echo: {}", input.prompt)));
        let input_tokens = estimate_tokens(&input.prompt);
        let output_tokens = estimate_tokens(&serde_json::to_string(&output)?);

        Ok(AgentProviderResult {
            output: output.clone(),
            session_id: None,
            model: super::common::option_model(&input.options),
            usage: Some(AgentUsage {
                input_tokens: Some(input_tokens),
                output_tokens: Some(output_tokens),
                total_tokens: Some(input_tokens + output_tokens),
                cost: Some(AgentUsageCost {
                    input: Some(0.0),
                    output: Some(0.0),
                    total: Some(0.0),
                    currency: Some("USD".to_string()),
                    ..AgentUsageCost::default()
                }),
                ..AgentUsage::default()
            }),
            isolation: None,
            raw: None,
        })
    }
}

pub fn generate_debug_value_from_schema(schema: &Value) -> Value {
    match schema {
        Value::Bool(true) => Value::String("debug".to_string()),
        Value::Bool(false) => Value::Null,
        Value::Object(object) => generate_debug_value_from_schema_object(object),
        _ => Value::String("debug".to_string()),
    }
}

fn generate_debug_value_from_schema_object(schema: &Map<String, Value>) -> Value {
    if let Some(value) = schema.get("const") {
        return value.clone();
    }

    if let Some(value) = schema
        .get("enum")
        .and_then(Value::as_array)
        .and_then(|values| values.first())
    {
        return value.clone();
    }

    for key in ["oneOf", "anyOf"] {
        if let Some(value) = schema
            .get(key)
            .and_then(Value::as_array)
            .and_then(|values| values.first())
        {
            return generate_debug_value_from_schema(value);
        }
    }

    if let Some(all_of) = schema.get("allOf").and_then(Value::as_array) {
        return merge_all_of(all_of);
    }

    match first_schema_type(schema.get("type")).unwrap_or_else(|| infer_schema_type(schema)) {
        "null" => Value::Null,
        "boolean" => Value::Bool(true),
        "integer" => debug_number(schema, true),
        "number" => debug_number(schema, false),
        "string" => Value::String(debug_string(schema)),
        "array" => debug_array(schema),
        "object" => debug_object(schema),
        _ => debug_object(schema),
    }
}

fn merge_all_of(schemas: &[Value]) -> Value {
    let values = schemas
        .iter()
        .map(generate_debug_value_from_schema)
        .collect::<Vec<_>>();

    if values.iter().all(Value::is_object) {
        let mut merged = Map::new();
        for value in values {
            if let Value::Object(object) = value {
                merged.extend(object);
            }
        }
        Value::Object(merged)
    } else {
        values.last().cloned().unwrap_or(Value::Null)
    }
}

fn first_schema_type(value: Option<&Value>) -> Option<&str> {
    match value {
        Some(Value::String(value)) => Some(value.as_str()),
        Some(Value::Array(values)) => values.first().and_then(Value::as_str),
        _ => None,
    }
}

fn infer_schema_type(schema: &Map<String, Value>) -> &'static str {
    if schema.contains_key("properties")
        || schema.contains_key("required")
        || schema.contains_key("additionalProperties")
    {
        "object"
    } else if schema.contains_key("items") || schema.contains_key("prefixItems") {
        "array"
    } else if schema.contains_key("minimum")
        || schema.contains_key("maximum")
        || schema.contains_key("multipleOf")
    {
        "number"
    } else if schema.contains_key("minLength")
        || schema.contains_key("maxLength")
        || schema.contains_key("pattern")
        || schema.contains_key("format")
    {
        "string"
    } else {
        "object"
    }
}

fn debug_number(schema: &Map<String, Value>, integer: bool) -> Value {
    let mut value = schema.get("minimum").and_then(Value::as_f64).unwrap_or(0.0);
    if let Some(exclusive_minimum) = schema.get("exclusiveMinimum").and_then(Value::as_f64) {
        value = value.max(exclusive_minimum + if integer { 1.0 } else { f64::EPSILON });
    }
    if integer || value.fract() == 0.0 {
        Value::Number((value.ceil() as i64).into())
    } else {
        serde_json::Number::from_f64(value)
            .map(Value::Number)
            .unwrap_or_else(|| Value::Number(0.into()))
    }
}

fn debug_string(schema: &Map<String, Value>) -> String {
    match schema.get("format").and_then(Value::as_str) {
        Some("email") => "debug@example.com",
        Some("uri" | "url") => "https://example.com/debug",
        Some("date-time") => "2000-01-01T00:00:00.000Z",
        Some("date") => "2000-01-01",
        _ => "debug-string",
    }
    .to_string()
}

fn debug_array(schema: &Map<String, Value>) -> Value {
    if let Some(prefix_items) = schema.get("prefixItems").and_then(Value::as_array) {
        if !prefix_items.is_empty() {
            return Value::Array(
                prefix_items
                    .iter()
                    .map(generate_debug_value_from_schema)
                    .collect(),
            );
        }
    }

    match schema.get("items") {
        Some(Value::Object(_)) | Some(Value::Bool(_)) => {
            Value::Array(vec![generate_debug_value_from_schema(&schema["items"])])
        }
        _ => Value::Array(vec![]),
    }
}

fn debug_object(schema: &Map<String, Value>) -> Value {
    let properties = schema
        .get("properties")
        .and_then(Value::as_object)
        .cloned()
        .unwrap_or_default();
    let mut keys = properties.keys().cloned().collect::<Vec<_>>();
    if let Some(required) = schema.get("required").and_then(Value::as_array) {
        for key in required.iter().filter_map(Value::as_str) {
            if !keys.iter().any(|existing| existing == key) {
                keys.push(key.to_string());
            }
        }
    }

    let mut output = Map::new();
    for key in keys {
        let value = properties.get(&key).unwrap_or(&Value::Bool(true));
        output.insert(key, generate_debug_value_from_schema(value));
    }
    Value::Object(output)
}

fn estimate_tokens(text: &str) -> u64 {
    std::cmp::max(1, text.len().div_ceil(4) as u64)
}