collet 0.1.1

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
//! Shared application state for the web server.

use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::sync::{Mutex, RwLock, broadcast};

use super::event::WebEvent;
use super::routes_swarm::SwarmAgentNode;
use crate::agent::approval::{ApproveMode, SharedApproveMode};
use crate::agent::context::ConversationContext;
use crate::agent::swarm::knowledge::SharedKnowledge;
use crate::api::provider::OpenAiCompatibleProvider;
use crate::config::Config;

/// Shared state accessible from all axum handlers.
pub struct AppState {
    pub config: Config,
    pub client: OpenAiCompatibleProvider,
    pub event_bus: broadcast::Sender<WebEvent>,
    pub agent_active: AtomicBool,
    pub approve_mode: SharedApproveMode,
    /// Current conversation context — persists across messages within a session.
    pub context: Mutex<Option<ConversationContext>>,
    /// Swarm agent graph state — updated in real-time from SSE events.
    pub swarm_agents: RwLock<HashMap<String, SwarmAgentNode>>,
    working_dir: RwLock<String>,
    /// Shared knowledge base — gives web handlers access to worker control handles.
    pub knowledge: RwLock<Option<SharedKnowledge>>,
}

impl AppState {
    pub fn new(
        config: Config,
        client: OpenAiCompatibleProvider,
        event_bus: broadcast::Sender<WebEvent>,
        working_dir: String,
    ) -> Self {
        let initial_mode = if config.yolo {
            ApproveMode::Yolo
        } else {
            ApproveMode::Auto
        };
        Self {
            config,
            client,
            event_bus,
            agent_active: AtomicBool::new(false),
            approve_mode: SharedApproveMode::new(initial_mode),
            context: Mutex::new(None),
            swarm_agents: RwLock::new(HashMap::new()),
            working_dir: RwLock::new(working_dir),
            knowledge: RwLock::new(None),
        }
    }

    /// Get the current working directory.
    pub async fn working_dir(&self) -> String {
        self.working_dir.read().await.clone()
    }

    /// Switch the active working directory.
    pub async fn set_working_dir(&self, dir: String) {
        *self.working_dir.write().await = dir;
    }

    pub fn is_agent_active(&self) -> bool {
        self.agent_active.load(Ordering::SeqCst)
    }

    pub fn set_agent_active(&self, active: bool) {
        self.agent_active.store(active, Ordering::SeqCst);
    }

    /// Atomically transition agent_active from false → true.
    ///
    /// Returns `true` if this caller successfully claimed the slot (was `false`, now `true`).
    /// Returns `false` if another concurrent caller already set it to `true`.
    /// Use this instead of `is_agent_active` + `set_agent_active` to avoid TOCTOU.
    pub fn try_claim_agent(&self) -> bool {
        self.agent_active
            .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
            .is_ok()
    }
}