stynx-code 3.3.1

stynx-code — interactive AI coding assistant
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::sync::atomic::AtomicU8;
use std::time::Instant;

use stynx_code_config::HooksConfig;
use stynx_code_types::PermissionChecker;
use stynx_code_tools::ToolRegistry;

pub enum AgentStatus {
    Running,
    Completed,
    Failed(String),
}

pub struct AgentRecord {
    pub id: String,
    pub name: String,
    pub task_preview: String,
    pub status: AgentStatus,
    pub result: Option<String>,
    pub started: Instant,
}

pub struct AgentManager {
    records: Mutex<HashMap<String, AgentRecord>>,
    channels: Mutex<HashMap<String, tokio::sync::oneshot::Receiver<Result<String, String>>>>,
    counter: Mutex<u64>,
}

impl AgentManager {
    pub fn new() -> Arc<Self> {
        Arc::new(Self {
            records: Mutex::new(HashMap::new()),
            channels: Mutex::new(HashMap::new()),
            counter: Mutex::new(1),
        })
    }

    fn next_id(&self) -> String {
        let mut n = self.counter.lock().unwrap();
        let id = format!("agent-{n}");
        *n += 1;
        id
    }

    pub fn register(&self, name: String, task: String) -> (String, tokio::sync::oneshot::Sender<Result<String, String>>) {
        let id = self.next_id();
        let (tx, rx) = tokio::sync::oneshot::channel();
        let preview = if task.len() > 80 { format!("{}", &task[..79]) } else { task.clone() };
        self.records.lock().unwrap().insert(id.clone(), AgentRecord {
            id: id.clone(), name, task_preview: preview,
            status: AgentStatus::Running, result: None, started: Instant::now(),
        });
        self.channels.lock().unwrap().insert(id.clone(), rx);
        (id, tx)
    }

    pub async fn wait_for(&self, id: &str) -> Result<String, String> {
        let rx = self.channels.lock().unwrap().remove(id);
        let result = if let Some(rx) = rx {
            rx.await.unwrap_or_else(|_| Err("agent task was dropped".into()))
        } else {
            self.records.lock().unwrap()
                .get(id).and_then(|r| r.result.clone()).map(Ok)
                .unwrap_or_else(|| Err(format!("agent '{id}' not found or already consumed")))
        };
        {
            let mut records = self.records.lock().unwrap();
            if let Some(rec) = records.get_mut(id) {
                match &result {
                    Ok(out) => { rec.status = AgentStatus::Completed; rec.result = Some(out.clone()); }
                    Err(e) => { rec.status = AgentStatus::Failed(e.clone()); }
                }
            }
        }
        result
    }

    pub fn snapshot(&self) -> Vec<AgentSnapshot> {
        self.records.lock().unwrap().values().map(|r| AgentSnapshot {
            id: r.id.clone(), name: r.name.clone(), task: r.task_preview.clone(),
            status: match &r.status {
                AgentStatus::Running => "running".into(),
                AgentStatus::Completed => "completed".into(),
                AgentStatus::Failed(e) => format!("failed: {e}"),
            },
            elapsed_s: r.started.elapsed().as_secs(),
        }).collect()
    }
}

#[derive(serde::Serialize)]
pub struct AgentSnapshot {
    pub id: String,
    pub name: String,
    pub task: String,
    pub status: String,
    pub elapsed_s: u64,
}

#[derive(Clone)]
pub(crate) struct AgentCtx {
    pub provider: Arc<dyn stynx_code_types::Provider>,
    pub registry: Arc<ToolRegistry>,
    pub permission: Arc<dyn PermissionChecker>,
    pub mode: Arc<AtomicU8>,
    pub hooks: HooksConfig,
}