loctree 0.13.1

Structural code intelligence for AI agents. Scan once, query everything.
Documentation
use super::{AicxIntent, ProjectScope};
use std::sync::mpsc;
use std::time::Duration;

#[derive(Debug)]
pub enum AicxInProcessError {
    TimedOut,
    Other(anyhow::Error),
}

#[derive(Debug)]
pub struct AicxInProcessClient {
    client: aicx::api::Aicx,
}

impl AicxInProcessClient {
    pub fn from_env() -> Result<Self, anyhow::Error> {
        let base = aicx::api::Aicx::from_env()?;
        let store_root = base.store_root().join("store");
        Ok(Self {
            client: aicx::api::Aicx::with_store_root(store_root),
        })
    }

    pub fn intents(
        &self,
        scope: &ProjectScope,
        window_hours: u64,
        limit: usize,
        cap: Option<Duration>,
    ) -> Result<Vec<AicxIntent>, AicxInProcessError> {
        let Some(cap) = cap else {
            return self
                .intents_unbounded(scope, window_hours, limit)
                .map_err(AicxInProcessError::Other);
        };

        let scope = scope.clone();
        let (tx, rx) = mpsc::channel();
        std::thread::spawn(move || {
            let result = Self::from_env()
                .and_then(|client| client.intents_unbounded(&scope, window_hours, limit));
            let _ = tx.send(result);
        });

        match rx.recv_timeout(cap) {
            Ok(Ok(rows)) => Ok(rows),
            Ok(Err(error)) => Err(AicxInProcessError::Other(error)),
            Err(mpsc::RecvTimeoutError::Timeout) => Err(AicxInProcessError::TimedOut),
            Err(mpsc::RecvTimeoutError::Disconnected) => Err(AicxInProcessError::Other(
                anyhow::anyhow!("in-process AICX worker disconnected"),
            )),
        }
    }

    fn intents_unbounded(
        &self,
        scope: &ProjectScope,
        window_hours: u64,
        limit: usize,
    ) -> Result<Vec<AicxIntent>, anyhow::Error> {
        let mut config = aicx::intents::IntentsConfig {
            project: scope.primary().to_string(),
            hours: window_hours,
            strict: false,
            min_confidence: None,
            kind_filter: None,
            frame_kind: Some(aicx::timeline::FrameKind::UserMsg),
        };
        let projects = scope.projects();
        let extraction = if projects.len() > 1 {
            let projects = projects.to_vec();
            self.client
                .extract_intents_for_projects(&config, &projects)?
        } else {
            if config.project.is_empty() {
                config.project = String::new();
            }
            self.client.extract_intents(&config)?
        };

        Ok(extraction
            .records
            .into_iter()
            .take(limit)
            .map(intent_record_to_loctree)
            .collect())
    }
}

fn intent_record_to_loctree(record: aicx::intents::IntentRecord) -> AicxIntent {
    AicxIntent {
        kind: intent_kind_label(record.kind).to_string(),
        text: record.summary,
        agent: record.agent,
        date: record.date,
        timestamp: record.timestamp,
        session_id: record.session_id,
        project: record.project,
        source_chunk_path: record.source_chunk,
        frame_kind: Some("user_msg".to_string()),
        oracle_status: None,
    }
}

fn intent_kind_label(kind: aicx::intents::IntentKind) -> &'static str {
    match kind {
        aicx::intents::IntentKind::Decision => "decision",
        aicx::intents::IntentKind::Intent => "intent",
        aicx::intents::IntentKind::Outcome => "outcome",
        aicx::intents::IntentKind::Task => "task",
    }
}