daat-locus 0.2.0

A long-running local agent runtime with memory, workflows, apps, and sleep-time self-improvement.
use crate::app::{AppHowToUse, AppId, AppUsage};

use super::prompt_text::{PromptTextBuilder, render_bullet_list};

mod generated {
    include!(concat!(env!("OUT_DIR"), "/prompt_bindings.rs"));
}

pub(crate) use generated::*;

pub(crate) fn prompt_bullet_lines(markdown: &str) -> Vec<String> {
    markdown
        .lines()
        .map(str::trim)
        .filter(|line| !line.is_empty())
        .map(|line| line.strip_prefix("- ").unwrap_or(line).to_string())
        .collect()
}

#[derive(Clone, Copy)]
pub(crate) struct AppPrompt {
    pub description: &'static str,
    pub when_to_use: &'static [&'static str],
    pub how_to_use: &'static str,
}

#[derive(Clone, Copy)]
pub(crate) struct PromptPersona {
    pub name: &'static str,
    pub language: &'static str,
    pub identity_summary: &'static str,
}

impl AppPrompt {
    pub(crate) fn usage(self) -> AppUsage {
        AppUsage {
            description: self.description.to_string(),
            when_to_use: self
                .when_to_use
                .iter()
                .map(|item| (*item).to_string())
                .collect(),
            body_markdown: None,
        }
    }

    pub(crate) fn app_how_to_use(self) -> AppHowToUse {
        AppHowToUse {
            lines: Vec::new(),
            body_markdown: Some(self.how_to_use.to_string()),
        }
    }
}

pub fn build_app_usage_prompt(_app_id: AppId, usage: &AppUsage) -> String {
    if let Some(body) = usage.body_markdown.as_deref()
        && !body.trim().is_empty()
    {
        return body.trim().to_string();
    }
    let mut builder = PromptTextBuilder::new();
    builder.push_labeled_section("description", usage.description.clone());
    if !usage.when_to_use.is_empty() {
        builder.push_labeled_section("when_to_use", render_bullet_list(usage.when_to_use.clone()));
    }
    builder.build()
}

pub fn build_app_how_to_use_prompt(app_id: AppId, how_to_use: &AppHowToUse) -> String {
    if let Some(body) = how_to_use.body_markdown.as_deref()
        && !body.trim().is_empty()
    {
        return body.trim().to_string();
    }
    let mut builder = PromptTextBuilder::new();
    let _ = app_id;
    builder.push_paragraph(render_bullet_list(how_to_use.lines.clone()));
    builder.build()
}

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

    #[test]
    fn generated_prompt_sources_include_markdown_assets() {
        assert!(
            generated::PROMPT_SOURCES
                .iter()
                .any(|(id, source)| *id == "system/core" && *source == SYSTEM_CORE)
        );
        assert!(
            generated::PROMPT_SOURCES
                .iter()
                .any(|(id, source)| *id == "persona/default" && *source == PERSONA_DEFAULT_SOURCE)
        );
    }

    #[test]
    fn prompt_bullet_lines_strips_markdown_bullets() {
        assert_eq!(
            prompt_bullet_lines("- first\n- second\n\nthird"),
            vec![
                "first".to_string(),
                "second".to_string(),
                "third".to_string()
            ]
        );
    }

    #[test]
    fn generated_app_prompt_structs_are_nonempty() {
        for prompt in [APP_BROWSER, APP_CODING, APP_TERMINAL] {
            assert!(!prompt.description.is_empty());
            assert!(!prompt.when_to_use.is_empty());
            assert!(!prompt.how_to_use.is_empty());
        }
    }

    #[test]
    fn generated_persona_prompt_struct_is_nonempty() {
        assert!(!PERSONA_DEFAULT.name.is_empty());
        assert!(!PERSONA_DEFAULT.language.is_empty());
        assert!(!PERSONA_DEFAULT.identity_summary.is_empty());
    }
}