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