agent-kernel 0.1.0

Minimal Agent orchestration kernel for multi-agent discussion
Documentation
use anyhow::Result;
use futures::future::join_all;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;

use crate::agent::Agent;
use crate::event::AgentEvent;
use crate::message::{Message, Role};
use crate::runtime::AgentRuntime;

/// The ONLY orchestration primitive in the kernel.
///
/// Runs a fixed-N-round multi-agent discussion, emitting [`AgentEvent`]s via
/// the provided channel. No convergence logic — runs exactly `rounds` rounds.
/// Caller controls policy through `rounds` and `cancel`.
///
/// The primary agent (`agents[0]`) provides the final summary.
pub async fn discuss(
    runtime: &dyn AgentRuntime,
    agents: &[Agent],
    topic: &str,
    rounds: usize,
    cancel: CancellationToken,
    events: mpsc::Sender<AgentEvent>,
) -> Result<String> {
    let mut history: Vec<Message> = vec![Message {
        role: Role::User,
        content: topic.to_string(),
    }];

    let mut primary_last_response = String::new();

    for round in 1..=rounds {
        if cancel.is_cancelled() {
            let _ = events.send(AgentEvent::Cancelled).await;
            return Ok(String::new());
        }

        let _ = events
            .send(AgentEvent::Progress {
                current_round: round,
                max_rounds: rounds,
            })
            .await;

        // Concurrent agent calls — each gets a snapshot of the shared history.
        // Snapshot is cloned per-agent so the owned Vec lives inside the async block,
        // satisfying the single-lifetime constraint of AgentRuntime::respond().
        let futs: Vec<_> = agents
            .iter()
            .map(|agent| {
                let snap = history.clone();
                async move {
                    runtime
                        .respond(agent, &snap)
                        .await
                        .map(|content| (agent.name.clone(), content))
                }
            })
            .collect();

        let results = join_all(futs).await;

        for (agent_name, content) in results.into_iter().flatten() {
            if agent_name == agents[0].name {
                primary_last_response = content.clone();
            }

            let _ = events
                .send(AgentEvent::Round {
                    round,
                    agent_name: agent_name.clone(),
                    content: content.clone(),
                })
                .await;

            history.push(Message {
                role: Role::User,
                content: format!("[{agent_name}]: {content}"),
            });
        }
    }

    let _ = events
        .send(AgentEvent::Summary {
            content: primary_last_response.clone(),
        })
        .await;
    let _ = events.send(AgentEvent::Completed).await;

    Ok(primary_last_response)
}