nyx-agent-ai 0.1.0

Implementation-detail AI runtime adapters, prompts, and automation helpers for nyx-agent.
Documentation
use serde_json::Value;

pub(crate) fn json_values_from_text(text: &str) -> Vec<Value> {
    let mut values = Vec::new();
    let mut offset = 0;
    while offset < text.len() {
        let Some((relative_start, ch)) =
            text[offset..].char_indices().find(|(_, ch)| matches!(ch, '{' | '['))
        else {
            break;
        };
        let start = offset + relative_start;
        let Some(end) = json_slice_end(text, start) else {
            offset = start + ch.len_utf8();
            continue;
        };
        if let Ok(value) = serde_json::from_str::<Value>(&text[start..end]) {
            values.push(value);
            offset = end;
        } else {
            offset = start + ch.len_utf8();
        }
    }
    values
}

fn json_slice_end(text: &str, start: usize) -> Option<usize> {
    let mut stack = Vec::new();
    let mut in_string = false;
    let mut escaped = false;

    for (idx, ch) in text[start..].char_indices() {
        if in_string {
            if escaped {
                escaped = false;
                continue;
            }
            match ch {
                '\\' => escaped = true,
                '"' => in_string = false,
                _ => {}
            }
            continue;
        }

        match ch {
            '"' => in_string = true,
            '{' => stack.push('}'),
            '[' => stack.push(']'),
            '}' | ']' => {
                if stack.pop() != Some(ch) {
                    return None;
                }
                if stack.is_empty() {
                    return Some(start + idx + ch.len_utf8());
                }
            }
            _ => {}
        }
    }
    None
}

pub(crate) fn optional_string(value: &Value, key: &str) -> Option<String> {
    value.get(key)?.as_str().map(|s| s.trim().to_string()).filter(|s| !s.is_empty())
}

pub(crate) fn string_array(value: &Value, key: &str) -> Vec<String> {
    value
        .get(key)
        .and_then(|v| v.as_array())
        .map(|items| {
            items
                .iter()
                .filter_map(|item| item.as_str())
                .map(str::trim)
                .filter(|item| !item.is_empty())
                .map(str::to_string)
                .collect()
        })
        .unwrap_or_default()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn extracts_pretty_json_from_markdown_fence() {
        let text = "done\n```json\n{\n  \"profile\": {\n    \"target_urls\": [\"http://127.0.0.1:3000\"]\n  }\n}\n```";
        let values = json_values_from_text(text);
        assert_eq!(values.len(), 1);
        assert_eq!(values[0]["profile"]["target_urls"][0], "http://127.0.0.1:3000");
    }

    #[test]
    fn ignores_braces_inside_strings() {
        let text = r#"{"summary":"kept { brace } in a string","warnings":[]}"#;
        let values = json_values_from_text(text);
        assert_eq!(values.len(), 1);
        assert_eq!(values[0]["summary"], "kept { brace } in a string");
    }
}