Skip to main content

comindly_core/
context.rs

1//! What a call into core needs, passed in rather than looked up: where the index lives, the
2//! settings it runs with, the LLM, where events go, and when to stop.
3
4use std::sync::Arc;
5
6use tokio_util::sync::CancellationToken;
7
8use crate::events::{EventSink, Reporter};
9use crate::llm::LlmClient;
10use crate::team::TeamSettings;
11use crate::worktree::{Credentials, NoCredentials};
12
13/// Settings resolved once at the edge: the index's shared settings, then the environment, then
14/// comindly's defaults.
15#[derive(Debug, Clone)]
16pub struct Settings {
17    pub llm_model: String,
18    pub llm_base_url: Option<String>,
19    pub embed_model: String,
20    pub embed: bool,
21    pub enrich: bool,
22    pub flows: bool,
23    pub guide: bool,
24    pub max_enrich: usize,
25    pub max_flows: usize,
26    pub llm_concurrency: usize,
27    /// USD per million `(input, output)` tokens.
28    pub prices: Option<(f64, f64)>,
29    /// What MCP tools return as text: `md` or `json`.
30    pub format: String,
31    pub scip: crate::scip::Mode,
32    pub auto_index: bool,
33    pub auto_index_llm: bool,
34    /// Time between checks whether a default branch moved.
35    pub auto_index_every: std::time::Duration,
36}
37
38/// Default and largest minutes between automatic re-index checks.
39pub const AUTO_INDEX_MINUTES: usize = 5;
40pub const MAX_AUTO_INDEX_MINUTES: usize = 1440;
41
42impl Settings {
43    /// `env` looks up overrides such as `COMINDLY_LLM_MODEL`.
44    pub fn resolve(team: &TeamSettings, env: impl Fn(&str) -> Option<String>) -> Settings {
45        let env = |k: &str| env(k).filter(|v| !v.is_empty());
46        Settings {
47            llm_model: team
48                .llm_model
49                .clone()
50                .or_else(|| env("COMINDLY_LLM_MODEL"))
51                .unwrap_or_else(|| crate::llm::DEFAULT_MODEL.to_string()),
52            llm_base_url: team
53                .llm_base_url
54                .clone()
55                .or_else(|| env("COMINDLY_LLM_BASE_URL")),
56            embed_model: team
57                .embed_model
58                .clone()
59                .or_else(|| env("COMINDLY_EMBED_MODEL"))
60                .unwrap_or_else(|| crate::embed::DEFAULT_MODEL.to_string()),
61            embed: team.embed.unwrap_or(true),
62            enrich: team.enrich.unwrap_or(true),
63            flows: team.flows.unwrap_or(true),
64            guide: team.guide.unwrap_or(true),
65            max_enrich: team.max_enrich.unwrap_or(usize::MAX),
66            max_flows: team.max_flows.unwrap_or(usize::MAX),
67            llm_concurrency: team.llm_concurrency.unwrap_or(8).clamp(1, 64),
68            prices: (team.llm_price_in.is_some() || team.llm_price_out.is_some()).then(|| {
69                (
70                    team.llm_price_in.unwrap_or(0.0),
71                    team.llm_price_out.unwrap_or(0.0),
72                )
73            }),
74            format: env("COMINDLY_FORMAT")
75                .or_else(|| team.format.clone())
76                .unwrap_or_else(|| "md".to_string()),
77            scip: env("COMINDLY_SCIP")
78                .or_else(|| team.scip.clone())
79                .and_then(|s| crate::scip::Mode::parse(&s))
80                .unwrap_or(crate::scip::Mode::Auto),
81            auto_index: team.auto_index.unwrap_or(false),
82            auto_index_llm: team.auto_index_llm.unwrap_or(false),
83            auto_index_every: std::time::Duration::from_secs(
84                60 * team
85                    .auto_index_minutes
86                    .unwrap_or(AUTO_INDEX_MINUTES)
87                    .clamp(1, MAX_AUTO_INDEX_MINUTES) as u64,
88            ),
89        }
90    }
91}
92
93/// Everything an indexing or query call needs.
94#[derive(Clone)]
95pub struct IndexContext {
96    /// The index location: a folder, `s3://`, `gs://` or `az://`.
97    pub root: String,
98    pub settings: Settings,
99    /// Absent: the LLM steps are skipped, and say so.
100    pub llm: Option<Arc<LlmClient>>,
101    /// Tokens for cloning and fetching over HTTPS.
102    pub git: Arc<dyn Credentials>,
103    pub report: Reporter,
104    pub cancel: CancellationToken,
105    /// Where comindly keeps its checkouts of each repository's default branch.
106    pub cache: std::path::PathBuf,
107}
108
109impl IndexContext {
110    pub fn new(
111        root: impl Into<String>,
112        settings: Settings,
113        events: Arc<dyn EventSink>,
114        cache: std::path::PathBuf,
115    ) -> Self {
116        IndexContext {
117            root: root.into(),
118            settings,
119            llm: None,
120            git: Arc::new(NoCredentials),
121            report: Reporter::new(events),
122            cancel: CancellationToken::new(),
123            cache,
124        }
125    }
126
127    pub fn with_llm(mut self, llm: Option<LlmClient>) -> Self {
128        self.llm = llm.map(Arc::new);
129        self
130    }
131
132    pub fn with_git(mut self, git: Arc<dyn Credentials>) -> Self {
133        self.git = git;
134        self
135    }
136
137    /// The graph tables: `<root>/_graph`.
138    pub fn graph(&self) -> String {
139        format!("{}/_graph", self.root.trim_end_matches('/'))
140    }
141
142    /// An error when the caller cancelled.
143    pub fn check(&self) -> anyhow::Result<()> {
144        anyhow::ensure!(!self.cancel.is_cancelled(), Cancelled);
145        Ok(())
146    }
147}
148
149/// The work was cancelled; what was saved before stays.
150#[derive(Debug, Clone, Copy)]
151pub struct Cancelled;
152
153impl std::fmt::Display for Cancelled {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        f.write_str("cancelled")
156    }
157}
158
159impl std::error::Error for Cancelled {}