collet 0.1.1

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
//! Knowledge summary generation and relevance filtering.

use crate::agent::swarm::knowledge::SharedKnowledge;
use crate::search::tokenizer;

impl SharedKnowledge {
    /// Build a summary of **all** shared knowledge (unfiltered).
    pub async fn build_context_summary(&self) -> String {
        let inner = self.inner.read().await;
        let mut parts = Vec::new();

        if !inner.files_read.is_empty() {
            parts.push("## Files Already Read\n".to_string());
            for (path, summary) in &inner.files_read {
                parts.push(format!(
                    "- `{path}` ({} lines): {}",
                    summary.line_count, summary.summary
                ));
            }
        }

        if !inner.facts.is_empty() {
            parts.push("\n## Shared Discoveries\n".to_string());
            for fact in inner.facts.values() {
                let age_secs = fact.timestamp.elapsed().as_secs();
                parts.push(format!(
                    "- [{}] (id={}, {}s ago) {}: {}",
                    fact.agent_id, fact.id, age_secs, fact.topic, fact.content
                ));
            }
        }

        if !inner.announcements.is_empty() {
            parts.push("\n## Announcements\n".to_string());
            for ann in inner.announcements.iter().rev().take(10) {
                let age_secs = ann.timestamp.elapsed().as_secs();
                parts.push(format!(
                    "- [{}] (id={}, {}s ago) {}",
                    ann.agent_id, ann.id, age_secs, ann.message
                ));
            }
        }

        if !inner.blackboard.is_empty() {
            parts.push("\n## Blackboard\n".to_string());
            for entry in inner.blackboard.values() {
                let votes = format!("+{} -{}", entry.votes_for.len(), entry.votes_against.len());
                let age_secs = entry.timestamp.elapsed().as_secs();
                parts.push(format!(
                    "- [{:?}] {} = {} (by {}, votes: {}, {}s ago)",
                    entry.kind, entry.key, entry.value, entry.author, votes, age_secs
                ));
            }
        }

        parts.join("\n")
    }

    /// Build a **relevance-filtered** summary for a specific subtask.
    pub async fn build_relevant_summary(
        &self,
        subtask_prompt: &str,
        target_files: &[String],
        max_items: usize,
    ) -> String {
        let inner = self.inner.read().await;

        if inner.index.is_empty() {
            return String::new();
        }

        let query_tokens = tokenizer::tokenize_query(subtask_prompt);
        if query_tokens.is_empty() {
            drop(inner);
            return self.build_context_summary().await;
        }

        let ranked = inner.rank_entries(&query_tokens, target_files, max_items);
        if ranked.is_empty() {
            return String::new();
        }

        // Group by kind for structured output
        let mut file_entries = Vec::new();
        let mut fact_entries = Vec::new();
        let mut ann_entries = Vec::new();

        for (idx, score) in &ranked {
            let entry = &inner.index[*idx];
            let line = format!("[{}] {} (relevance: {:.1})", entry.id, entry.display, score);
            match entry.kind {
                crate::agent::swarm::knowledge::types::EntryKind::FileRead => {
                    file_entries.push(line)
                }
                crate::agent::swarm::knowledge::types::EntryKind::Fact => fact_entries.push(line),
                crate::agent::swarm::knowledge::types::EntryKind::Announcement => {
                    ann_entries.push(line)
                }
            }
        }

        let mut parts = Vec::new();
        if !file_entries.is_empty() {
            parts.push("## Relevant Files Read\n".to_string());
            parts.extend(file_entries);
        }
        if !fact_entries.is_empty() {
            parts.push("\n## Relevant Discoveries\n".to_string());
            parts.extend(fact_entries);
        }
        if !ann_entries.is_empty() {
            parts.push("\n## Recent Announcements\n".to_string());
            parts.extend(ann_entries);
        }

        parts.join("\n")
    }

    /// Batch-score multiple subtasks in a single read-lock acquisition.
    ///
    /// Returns one knowledge hint string per task (same order as input).
    /// This avoids N separate lock acquisitions when dispatching N workers
    /// in the same wave.
    pub async fn batch_relevant_summaries(
        &self,
        tasks: &[(String, Vec<String>)], // (subtask_prompt, target_files)
        max_items: usize,
    ) -> Vec<String> {
        let inner = self.inner.read().await;

        if inner.index.is_empty() {
            return vec![String::new(); tasks.len()];
        }

        tasks
            .iter()
            .map(|(prompt, target_files)| {
                let query_tokens = tokenizer::tokenize_query(prompt);
                if query_tokens.is_empty() {
                    return String::new();
                }

                let ranked = inner.rank_entries(&query_tokens, target_files, max_items);
                if ranked.is_empty() {
                    return String::new();
                }

                // Group by kind for structured output (same as build_relevant_summary)
                let mut file_entries = Vec::new();
                let mut fact_entries = Vec::new();
                let mut ann_entries = Vec::new();

                for (idx, score) in &ranked {
                    let entry = &inner.index[*idx];
                    let line = format!("{} (relevance: {:.1})", entry.display, score);
                    match entry.kind {
                        crate::agent::swarm::knowledge::types::EntryKind::FileRead => {
                            file_entries.push(line)
                        }
                        crate::agent::swarm::knowledge::types::EntryKind::Fact => {
                            fact_entries.push(line)
                        }
                        crate::agent::swarm::knowledge::types::EntryKind::Announcement => {
                            ann_entries.push(line)
                        }
                    }
                }

                let mut parts = Vec::new();
                if !file_entries.is_empty() {
                    parts.push("## Relevant Files Read\n".to_string());
                    parts.extend(file_entries);
                }
                if !fact_entries.is_empty() {
                    parts.push("\n## Relevant Discoveries\n".to_string());
                    parts.extend(fact_entries);
                }
                if !ann_entries.is_empty() {
                    parts.push("\n## Recent Announcements\n".to_string());
                    parts.extend(ann_entries);
                }

                parts.join("\n")
            })
            .collect()
    }
}