lha 1.0.2

Long-Horizon Agent command-line package that installs the lha binary.
Documentation
use crate::product::agent::config::types::TuiBuddy;

pub(crate) const BUDDY_COMPANION_DISABLED_INSTRUCTIONS: &str = "\
<buddy_companion>
The TUI buddy companion is currently inactive. Ignore any previous buddy_companion instructions.
</buddy_companion>";

fn buddy_identity(buddy: &TuiBuddy) -> Option<(&str, String)> {
    if !buddy.enabled || buddy.muted || !buddy.observer.enabled {
        return None;
    }

    let name = buddy.name.as_deref()?.trim();
    if name.is_empty() {
        return None;
    }
    let species = buddy.species.map(|species| species.to_string())?;
    Some((name, species))
}

fn personality_sentence(name: &str, personality: Option<&str>) -> Option<String> {
    let personality = personality
        .map(str::trim)
        .filter(|value| !value.is_empty())?;
    Some(format!("{name} has a {personality} temperament."))
}

pub(crate) fn buddy_model_instructions(buddy: &TuiBuddy) -> Option<String> {
    let (name, species) = buddy_identity(buddy)?;
    let personality = personality_sentence(name, buddy.personality.as_deref());
    let personality = personality
        .map(|value| format!(" {value}"))
        .unwrap_or_default();
    Some(format!(
        "<buddy_companion>\n\
This is the current TUI buddy companion context and replaces any previous buddy_companion context.\n\
A small {species} named {name} sits beside the user's input box and occasionally comments in a speech bubble.{personality}\n\n\
You are not {name}; it is a separate UI companion. When the user addresses {name} directly, stay out of the way: respond in one line or less, or answer only the part meant for you. Do not narrate what {name} might say; the bubble handles that.\n\
</buddy_companion>"
    ))
}

#[cfg(test)]
mod tests {
    use crate::product::agent::config::types::BuddyObserverConfig;
    use crate::product::agent::config::types::BuddySpecies;
    use crate::product::agent::config::types::TuiBuddy;

    use super::*;

    fn buddy() -> TuiBuddy {
        TuiBuddy {
            enabled: true,
            muted: false,
            name: Some("Byte".to_string()),
            species: Some(BuddySpecies::Duck),
            personality: Some("quiet optimizer".to_string()),
            observer: BuddyObserverConfig {
                enabled: true,
                ..BuddyObserverConfig::default()
            },
            ..TuiBuddy::default()
        }
    }

    #[test]
    fn model_instructions_are_omitted_when_talk_is_off() {
        let buddy = TuiBuddy {
            observer: BuddyObserverConfig {
                enabled: false,
                ..BuddyObserverConfig::default()
            },
            ..buddy()
        };

        assert_eq!(buddy_model_instructions(&buddy), None);
    }
}