haki-agents 0.1.0

Agent definitions, spawner, and concurrent subagent dispatch for haki
Documentation
//! Agent spawner — runs agents as concurrent tokio tasks.

use tokio::sync::oneshot;
use haki_core::{AgentLoop, NoopToolExecutor, Session};
use haki_config::Config;
use haki_llm::LlmProvider;

use crate::definition::AgentDef;

/// A handle to a running agent task.
pub struct AgentHandle {
    pub def: AgentDef,
    result_rx: oneshot::Receiver<anyhow::Result<String>>,
}

impl AgentHandle {
    /// Wait for the agent to finish and return its final reply.
    pub async fn await_result(self) -> anyhow::Result<String> {
        self.result_rx.await.map_err(|_| anyhow::anyhow!("Agent task dropped"))?
    }
}

pub struct AgentSpawner {
    config: Config,
}

impl AgentSpawner {
    pub fn new(config: Config) -> Self {
        Self { config }
    }

    /// Spawn an agent to answer a single prompt asynchronously.
    pub fn spawn(&self, def: AgentDef, prompt: String) -> anyhow::Result<AgentHandle> {
        let (tx, rx) = oneshot::channel();

        let model = def
            .model_override
            .clone()
            .unwrap_or_else(|| self.config.model.clone());

        let mut cfg = self.config.clone();
        cfg.model = model;
        cfg.system_prompt = Some(def.role.system_prompt().to_string());

        let provider = LlmProvider::from_config(&cfg.provider)?;

        tokio::spawn(async move {
            let mut session = Session::new(cfg);
            let agent = AgentLoop::new(provider, Box::new(NoopToolExecutor), 4096);
            let result = agent.run_turn(&mut session, &prompt).await;
            let _ = tx.send(result);
        });

        Ok(AgentHandle { def, result_rx: rx })
    }
}

// ─── Tests ────────────────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::*;
    use crate::definition::AgentRole;
    use haki_config::Config;

    #[test]
    fn spawner_builds_without_panic() {
        let _ = AgentSpawner::new(Config::default());
    }

    #[test]
    fn spawn_fails_gracefully_with_bad_provider() {
        let mut cfg = Config::default();
        // Force an unknown provider so from_config always errors regardless of env.
        cfg.provider.name = "nonexistent-xyz".into();
        cfg.provider.api_key = Some("fake".into());
        let spawner = AgentSpawner::new(cfg);
        let def = AgentDef::new("test", AgentRole::Planner);
        assert!(spawner.spawn(def, "hello".into()).is_err());
    }
}