tandem-server 0.4.18

HTTP server for Tandem engine APIs
Documentation
use super::super::truncate_text;
use super::types::AutomationVerificationStep;
use super::*;
use serde_json::{json, Value};
use tandem_types::{MessagePart, Session};

pub(crate) fn automation_node_verification_state(node: &AutomationFlowNode) -> Option<String> {
    automation_node_builder_metadata(node, "verification_state")
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
}

pub(crate) fn automation_node_verification_command(node: &AutomationFlowNode) -> Option<String> {
    automation_node_builder_metadata(node, "verification_command")
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
}

pub(crate) fn infer_verification_kind(command: &str) -> String {
    let lowered = command.trim().to_ascii_lowercase();
    if lowered.is_empty() {
        return "verify".to_string();
    }
    if lowered.starts_with("build:")
        || lowered.contains(" cargo build")
        || lowered.starts_with("cargo build")
        || lowered.contains(" npm run build")
        || lowered.starts_with("npm run build")
        || lowered.contains(" pnpm build")
        || lowered.starts_with("pnpm build")
        || lowered.contains(" yarn build")
        || lowered.starts_with("yarn build")
        || lowered.contains(" tsc")
        || lowered.starts_with("tsc")
        || lowered.starts_with("cargo check")
        || lowered.contains(" cargo check")
    {
        return "build".to_string();
    }
    if lowered.starts_with("test:")
        || lowered.contains(" cargo test")
        || lowered.starts_with("cargo test")
        || lowered.contains(" pytest")
        || lowered.starts_with("pytest")
        || lowered.contains(" npm test")
        || lowered.starts_with("npm test")
        || lowered.contains(" pnpm test")
        || lowered.starts_with("pnpm test")
        || lowered.contains(" yarn test")
        || lowered.starts_with("yarn test")
        || lowered.contains(" go test")
        || lowered.starts_with("go test")
    {
        return "test".to_string();
    }
    if lowered.starts_with("lint:")
        || lowered.contains(" clippy")
        || lowered.starts_with("cargo clippy")
        || lowered.contains(" eslint")
        || lowered.starts_with("eslint")
        || lowered.contains(" ruff")
        || lowered.starts_with("ruff")
        || lowered.contains(" shellcheck")
        || lowered.starts_with("shellcheck")
        || lowered.contains(" fmt --check")
        || lowered.contains(" format")
        || lowered.contains(" lint")
    {
        return "lint".to_string();
    }
    "verify".to_string()
}

pub(crate) fn split_verification_commands(raw: &str) -> Vec<String> {
    let mut commands = Vec::new();
    for line in raw.lines() {
        let trimmed = line.trim();
        if trimmed.is_empty() {
            continue;
        }
        for chunk in trimmed.split("&&") {
            for piece in chunk.split(';') {
                let candidate = piece.trim();
                if candidate.is_empty() {
                    continue;
                }
                commands.push(candidate.to_string());
            }
        }
    }
    let mut seen = std::collections::HashSet::new();
    commands
        .into_iter()
        .filter(|value| seen.insert(value.to_ascii_lowercase()))
        .collect()
}

pub(crate) fn automation_node_verification_plan(
    node: &AutomationFlowNode,
) -> Vec<AutomationVerificationStep> {
    if let Some(items) = node
        .metadata
        .as_ref()
        .and_then(|metadata| metadata.get("builder"))
        .and_then(Value::as_object)
        .and_then(|builder| builder.get("verification_plan"))
        .and_then(Value::as_array)
    {
        let mut plan = Vec::new();
        for item in items {
            let (kind, command) = if let Some(obj) = item.as_object() {
                let command = obj
                    .get("command")
                    .or_else(|| obj.get("value"))
                    .and_then(Value::as_str)
                    .map(str::trim)
                    .filter(|value| !value.is_empty())
                    .map(str::to_string);
                let kind = obj
                    .get("kind")
                    .and_then(Value::as_str)
                    .map(str::trim)
                    .filter(|value| !value.is_empty())
                    .map(str::to_ascii_lowercase);
                (kind, command)
            } else {
                (
                    None,
                    item.as_str()
                        .map(str::trim)
                        .filter(|value| !value.is_empty())
                        .map(str::to_string),
                )
            };
            let Some(command) = command else {
                continue;
            };
            plan.push(AutomationVerificationStep {
                kind: kind.unwrap_or_else(|| infer_verification_kind(&command)),
                command,
            });
        }
        if !plan.is_empty() {
            return plan;
        }
    }
    automation_node_verification_command(node)
        .map(|raw| {
            split_verification_commands(&raw)
                .into_iter()
                .map(|command| AutomationVerificationStep {
                    kind: infer_verification_kind(&command),
                    command,
                })
                .collect::<Vec<_>>()
        })
        .unwrap_or_default()
}

pub(crate) fn session_verification_summary(node: &AutomationFlowNode, session: &Session) -> Value {
    let verification_plan = automation_node_verification_plan(node);
    let Some(expected_command) = automation_node_verification_command(node) else {
        return json!({
            "verification_expected": false,
            "verification_command": Value::Null,
            "verification_plan": [],
            "verification_results": [],
            "verification_outcome": Value::Null,
            "verification_total": 0,
            "verification_completed": 0,
            "verification_passed_count": 0,
            "verification_failed_count": 0,
            "verification_ran": false,
            "verification_failed": false,
            "latest_verification_command": Value::Null,
            "latest_verification_failure": Value::Null,
        });
    };
    let verification_plan = if verification_plan.is_empty() {
        vec![AutomationVerificationStep {
            kind: infer_verification_kind(&expected_command),
            command: expected_command.clone(),
        }]
    } else {
        verification_plan
    };
    let mut verification_results = verification_plan
        .iter()
        .map(|step| {
            json!({
                "kind": step.kind,
                "command": step.command,
                "ran": false,
                "failed": false,
                "failure": Value::Null,
                "latest_command": Value::Null,
            })
        })
        .collect::<Vec<_>>();
    let mut verification_ran = false;
    let mut verification_failed = false;
    let mut latest_verification_command = None::<String>;
    let mut latest_verification_failure = None::<String>;
    for message in &session.messages {
        for part in &message.parts {
            let MessagePart::ToolInvocation {
                tool,
                args,
                result,
                error,
            } = part
            else {
                continue;
            };
            if tool.trim().to_ascii_lowercase().replace('-', "_") != "bash" {
                continue;
            }
            let Some(command) = args.get("command").and_then(Value::as_str).map(str::trim) else {
                continue;
            };
            let command_normalized = command.to_ascii_lowercase();
            let failure = if let Some(error) = error
                .as_deref()
                .map(str::trim)
                .filter(|value| !value.is_empty())
            {
                Some(error.to_string())
            } else {
                let metadata = result
                    .as_ref()
                    .and_then(|value| value.get("metadata"))
                    .cloned()
                    .unwrap_or(Value::Null);
                let exit_code = metadata.get("exit_code").and_then(Value::as_i64);
                let timed_out = metadata
                    .get("timeout")
                    .and_then(Value::as_bool)
                    .unwrap_or(false);
                let cancelled = metadata
                    .get("cancelled")
                    .and_then(Value::as_bool)
                    .unwrap_or(false);
                let stderr = metadata
                    .get("stderr")
                    .and_then(Value::as_str)
                    .map(str::trim)
                    .filter(|value| !value.is_empty())
                    .map(str::to_string);
                if timed_out {
                    Some(format!("verification command timed out: {}", command))
                } else if cancelled {
                    Some(format!("verification command was cancelled: {}", command))
                } else if exit_code.is_some_and(|code| code != 0) {
                    Some(
                        stderr
                            .filter(|value| !value.is_empty())
                            .map(|value| {
                                format!(
                                    "verification command failed with exit code {}: {}",
                                    exit_code.unwrap_or_default(),
                                    truncate_text(&value, 240)
                                )
                            })
                            .unwrap_or_else(|| {
                                format!(
                                    "verification command failed with exit code {}: {}",
                                    exit_code.unwrap_or_default(),
                                    command
                                )
                            }),
                    )
                } else {
                    None
                }
            };
            for result in &mut verification_results {
                let Some(expected) = result.get("command").and_then(Value::as_str) else {
                    continue;
                };
                let expected_normalized = expected.trim().to_ascii_lowercase();
                if !command_normalized.contains(&expected_normalized) {
                    continue;
                }
                verification_ran = true;
                latest_verification_command = Some(command.to_string());
                if let Some(object) = result.as_object_mut() {
                    object.insert("ran".to_string(), json!(true));
                    object.insert("latest_command".to_string(), json!(command.to_string()));
                    if let Some(failure_text) = failure.clone() {
                        verification_failed = true;
                        latest_verification_failure = Some(failure_text.clone());
                        object.insert("failed".to_string(), json!(true));
                        object.insert("failure".to_string(), json!(failure_text));
                    }
                }
            }
        }
    }
    let verification_completed = verification_results
        .iter()
        .filter(|value| value.get("ran").and_then(Value::as_bool).unwrap_or(false))
        .count();
    let verification_failed_count = verification_results
        .iter()
        .filter(|value| {
            value
                .get("failed")
                .and_then(Value::as_bool)
                .unwrap_or(false)
        })
        .count();
    let verification_passed_count = verification_results
        .iter()
        .filter(|value| {
            value.get("ran").and_then(Value::as_bool).unwrap_or(false)
                && !value
                    .get("failed")
                    .and_then(Value::as_bool)
                    .unwrap_or(false)
        })
        .count();
    let verification_total = verification_results.len();
    let verification_outcome = if verification_total == 0 {
        None
    } else if verification_failed_count > 0 {
        Some("failed")
    } else if verification_completed == 0 {
        Some("missing")
    } else if verification_completed < verification_total {
        Some("partial")
    } else {
        Some("passed")
    };
    json!({
        "verification_expected": true,
        "verification_command": expected_command,
        "verification_plan": verification_plan
            .iter()
            .map(|step| json!({"kind": step.kind, "command": step.command}))
            .collect::<Vec<_>>(),
        "verification_results": verification_results,
        "verification_outcome": verification_outcome,
        "verification_total": verification_total,
        "verification_completed": verification_completed,
        "verification_passed_count": verification_passed_count,
        "verification_failed_count": verification_failed_count,
        "verification_ran": verification_ran,
        "verification_failed": verification_failed,
        "latest_verification_command": latest_verification_command,
        "latest_verification_failure": latest_verification_failure,
    })
}