1use std::path::PathBuf;
2use std::time::Duration;
3
4use async_trait::async_trait;
5use code_system_graph_model::RepoId;
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9use tokio_util::sync::CancellationToken;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum ProviderStatus {
15 Available,
17 IndexMissing,
19 Stale,
21 Unavailable,
23 Incompatible,
25 InvalidResponse,
27 TimedOut,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
33#[serde(rename_all = "snake_case")]
34pub enum ProviderTransport {
35 Mcp,
37 Cli,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
43#[serde(rename_all = "snake_case")]
44pub enum ProviderOperation {
45 ResolveSymbols,
47 LocalNeighbors,
49 LocalImpact,
51 LocalContext,
53 AffectedTests,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub struct ProviderOperationCapability {
60 pub operation: ProviderOperation,
62 pub transport: ProviderTransport,
64 pub public_name: String,
66 pub parameters: Vec<String>,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
72pub struct ProviderDegradation {
73 pub kind: String,
75 pub message: String,
77 pub diagnostics_id: Option<String>,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83pub struct ProviderCapability {
84 pub provider: String,
86 pub version: Option<String>,
88 pub status: ProviderStatus,
90 pub tools: Vec<String>,
92 pub protocol_version: Option<String>,
94 pub operations: Vec<ProviderOperationCapability>,
96 pub degradations: Vec<ProviderDegradation>,
98 pub remediation: Option<String>,
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub struct ProviderBudget {
105 pub timeout: Duration,
107 pub max_output_bytes: usize,
109 pub max_items: usize,
111}
112
113impl Default for ProviderBudget {
114 fn default() -> Self {
115 Self {
116 timeout: Duration::from_secs(5),
117 max_output_bytes: 1024 * 1024,
118 max_items: 50,
119 }
120 }
121}
122
123#[derive(Debug, Clone)]
125pub struct ProviderRequest {
126 pub repo_id: RepoId,
128 pub project_path: PathBuf,
130 pub budget: ProviderBudget,
132 pub cancellation: CancellationToken,
134}
135
136#[derive(Debug, Clone)]
138pub struct ResolveSymbolsRequest {
139 pub request: ProviderRequest,
141 pub query: String,
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
147#[serde(rename_all = "snake_case")]
148pub enum LocalNeighborDirection {
149 Callers,
151 Callees,
153}
154
155#[derive(Debug, Clone)]
157pub struct LocalNeighborsRequest {
158 pub request: ProviderRequest,
160 pub symbol: String,
162 pub direction: LocalNeighborDirection,
164}
165
166#[derive(Debug, Clone)]
168pub struct LocalImpactRequest {
169 pub request: ProviderRequest,
171 pub symbol: String,
173 pub max_depth: usize,
175}
176
177#[derive(Debug, Clone)]
179pub struct LocalContextRequest {
180 pub request: ProviderRequest,
182 pub query: String,
184 pub max_files: usize,
186}
187
188#[derive(Debug, Clone)]
190pub struct AffectedTestsRequest {
191 pub request: ProviderRequest,
193 pub changed_files: Vec<String>,
195 pub max_depth: usize,
197}
198
199#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
201pub struct ProviderExecution {
202 pub transport: ProviderTransport,
204 pub output_bytes: usize,
206 pub truncated: bool,
208 pub degradations: Vec<ProviderDegradation>,
210}
211
212#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
214pub struct ResolvedSymbol {
215 pub local_id: Option<String>,
217 pub name: String,
219 pub qualified_name: Option<String>,
221 pub kind: String,
223 pub file_path: String,
225 pub start_line: usize,
227 pub score: Option<f64>,
229}
230
231#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
233pub struct ResolveSymbolsResult {
234 pub symbols: Vec<ResolvedSymbol>,
236 pub execution: ProviderExecution,
238}
239
240#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
242pub struct LocalNeighbor {
243 pub name: String,
245 pub kind: String,
247 pub file_path: String,
249 pub start_line: usize,
251}
252
253#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
255pub struct LocalNeighborResult {
256 pub symbol: String,
258 pub direction: LocalNeighborDirection,
260 pub neighbors: Vec<LocalNeighbor>,
262 pub execution: ProviderExecution,
264}
265
266#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
268pub struct LocalImpactResult {
269 pub symbol: String,
271 pub depth: usize,
273 pub provider_node_count: usize,
275 pub affected: Vec<LocalNeighbor>,
277 pub execution: ProviderExecution,
279}
280
281#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
283pub struct LocalContextResult {
284 pub content: String,
286 pub execution: ProviderExecution,
288}
289
290#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
292pub struct AffectedTestsResult {
293 pub changed_files: Vec<String>,
295 pub affected_tests: Vec<String>,
297 pub total_dependents_traversed: usize,
299 pub execution: ProviderExecution,
301}
302
303#[derive(Debug, Error, PartialEq, Eq)]
305pub enum ProviderError {
306 #[error("local intelligence request was cancelled")]
308 Cancelled,
309 #[error("local intelligence provider timed out (diagnostics: {diagnostics_id})")]
311 Timeout {
312 diagnostics_id: String,
314 },
315 #[error(
317 "local intelligence provider exceeded the {limit_bytes}-byte output limit (diagnostics: {diagnostics_id})"
318 )]
319 OutputLimit {
320 limit_bytes: usize,
322 diagnostics_id: String,
324 },
325 #[error("local intelligence provider circuit is temporarily open")]
327 CircuitOpen,
328 #[error("invalid local intelligence request: {0}")]
330 InvalidRequest(String),
331 #[error("invalid local intelligence response: {message} (diagnostics: {diagnostics_id})")]
333 InvalidResponse {
334 message: String,
336 diagnostics_id: String,
338 },
339 #[error("local intelligence transport failed: {message} (diagnostics: {diagnostics_id})")]
341 Transport {
342 message: String,
344 diagnostics_id: String,
346 },
347}
348
349#[async_trait]
351pub trait LocalCodeIntelligenceProvider: Send + Sync {
352 fn provider_name(&self) -> &'static str;
354
355 async fn probe(&self, request: ProviderRequest) -> Result<ProviderCapability, ProviderError>;
361
362 async fn resolve_symbols(
368 &self,
369 input: ResolveSymbolsRequest,
370 ) -> Result<ResolveSymbolsResult, ProviderError>;
371
372 async fn get_local_neighbors(
378 &self,
379 input: LocalNeighborsRequest,
380 ) -> Result<LocalNeighborResult, ProviderError>;
381
382 async fn get_local_impact(
388 &self,
389 input: LocalImpactRequest,
390 ) -> Result<LocalImpactResult, ProviderError>;
391
392 async fn build_local_context(
398 &self,
399 input: LocalContextRequest,
400 ) -> Result<LocalContextResult, ProviderError>;
401
402 async fn get_affected_tests(
410 &self,
411 input: AffectedTestsRequest,
412 ) -> Result<Option<AffectedTestsResult>, ProviderError>;
413
414 async fn shutdown(&self) -> Result<(), ProviderError>;
420}
421
422#[cfg(test)]
423mod tests {
424 use std::path::PathBuf;
425
426 use async_trait::async_trait;
427 use code_system_graph_model::RepoId;
428 use tokio_util::sync::CancellationToken;
429
430 use super::{
431 AffectedTestsRequest, AffectedTestsResult, LocalCodeIntelligenceProvider, LocalContextRequest, LocalContextResult, LocalImpactRequest, LocalImpactResult, LocalNeighborResult, LocalNeighborsRequest, ProviderBudget, ProviderCapability, ProviderError, ProviderRequest, ProviderStatus, ResolveSymbolsRequest, ResolveSymbolsResult
432 };
433
434 struct FakeProvider;
435
436 #[async_trait]
437 impl LocalCodeIntelligenceProvider for FakeProvider {
438 fn provider_name(&self) -> &'static str {
439 "fake"
440 }
441
442 async fn probe(
443 &self,
444 request: ProviderRequest,
445 ) -> Result<ProviderCapability, ProviderError> {
446 if request.cancellation.is_cancelled() {
447 return Err(ProviderError::Cancelled);
448 }
449 Ok(ProviderCapability {
450 provider: self.provider_name().to_owned(),
451 version: None,
452 status: ProviderStatus::IndexMissing,
453 tools: Vec::new(),
454 protocol_version: None,
455 operations: Vec::new(),
456 degradations: Vec::new(),
457 remediation: Some("Create the index explicitly with CodeGraph.".to_owned()),
458 })
459 }
460
461 async fn resolve_symbols(
462 &self,
463 _input: ResolveSymbolsRequest,
464 ) -> Result<ResolveSymbolsResult, ProviderError> {
465 Err(ProviderError::CircuitOpen)
466 }
467
468 async fn get_local_neighbors(
469 &self,
470 _input: LocalNeighborsRequest,
471 ) -> Result<LocalNeighborResult, ProviderError> {
472 Err(ProviderError::CircuitOpen)
473 }
474
475 async fn get_local_impact(
476 &self,
477 _input: LocalImpactRequest,
478 ) -> Result<LocalImpactResult, ProviderError> {
479 Err(ProviderError::CircuitOpen)
480 }
481
482 async fn build_local_context(
483 &self,
484 _input: LocalContextRequest,
485 ) -> Result<LocalContextResult, ProviderError> {
486 Err(ProviderError::CircuitOpen)
487 }
488
489 async fn get_affected_tests(
490 &self,
491 _input: AffectedTestsRequest,
492 ) -> Result<Option<AffectedTestsResult>, ProviderError> {
493 Ok(None)
494 }
495
496 async fn shutdown(&self) -> Result<(), ProviderError> {
497 Ok(())
498 }
499 }
500
501 #[tokio::test]
502 async fn probe_should_preserve_missing_index_degradation() {
503 let result = FakeProvider
504 .probe(ProviderRequest {
505 repo_id: RepoId::new("repo:web"),
506 project_path: PathBuf::from("/workspace/web"),
507 budget: ProviderBudget::default(),
508 cancellation: CancellationToken::new(),
509 })
510 .await;
511 let status = result.map(|capability| capability.status);
512
513 assert_eq!(status, Ok(ProviderStatus::IndexMissing));
514 }
515}