1use tonic::{Response, Status};
2use tracing::info;
3
4use crate::server::ProtocolServer;
5use crate::{
6 CallEdgeRef, ContextDepth, ContextRequest, ContextResponse, SymbolRef, SymbolResult,
7};
8
9pub async fn handle_context(
22 server: &ProtocolServer,
23 req: ContextRequest,
24) -> Result<Response<ContextResponse>, Status> {
25 let session = server.validate_session(&req.session_id)?;
27 crate::require_live_session::require_live_session(server, &req.session_id).await?;
28
29 let sid = req
31 .session_id
32 .parse::<uuid::Uuid>()
33 .map_err(|_| Status::invalid_argument("Invalid session ID"))?;
34 server.session_mgr().touch_session(&sid);
35
36 let depth = req.depth();
40 let include_source = depth == ContextDepth::Full || depth == ContextDepth::CallGraph;
41 let include_call_graph = depth == ContextDepth::CallGraph;
42
43 let max_results = if req.max_tokens > 0 {
44 ((req.max_tokens / 100) as usize).max(10)
45 } else {
46 50
47 };
48
49 let engine = server.engine();
50
51 let (symbol_results, call_edges) = {
52 let (repo_id, git_repo) = engine
53 .get_repo(&session.codebase)
54 .await
55 .map_err(|e| Status::internal(format!("Repo error: {e}")))?;
56
57 let symbols = engine
58 .query_symbols(repo_id, &req.query, max_results)
59 .await
60 .map_err(|e| Status::internal(format!("Query error: {e}")))?;
61
62 let maybe_ws = engine.workspace_manager().get_workspace(&sid);
66
67 let mut results = Vec::with_capacity(symbols.len());
68 let mut edges = Vec::new();
69
70 for sym in &symbols {
71 let mut result = SymbolResult {
72 symbol: Some(symbol_to_ref(sym)),
73 source: None,
74 caller_ids: vec![],
75 callee_ids: vec![],
76 test_symbol_ids: vec![],
77 };
78
79 if include_source {
81 let source_bytes = if let Some(ref ws) = maybe_ws {
82 ws.read_file(
84 &sym.file_path.to_string_lossy(),
85 &git_repo,
86 )
87 .ok()
88 .map(|r| r.content)
89 } else {
90 let file_path = git_repo.path().join(&sym.file_path);
92 std::fs::read(&file_path).ok()
93 };
94
95 if let Some(source) = source_bytes {
96 let start = sym.span.start_byte as usize;
97 let end = sym.span.end_byte as usize;
98 if end <= source.len() {
99 result.source = Some(
100 String::from_utf8_lossy(&source[start..end]).to_string(),
101 );
102 }
103 }
104 }
105
106 if include_call_graph {
108 if let Ok((callers, callees)) = engine.get_call_graph(repo_id, sym.id).await {
110 result.caller_ids = callers.iter().map(|s| s.id.to_string()).collect();
111 result.callee_ids = callees.iter().map(|s| s.id.to_string()).collect();
112
113 for caller in &callers {
114 edges.push(CallEdgeRef {
115 caller_id: caller.id.to_string(),
116 callee_id: sym.id.to_string(),
117 kind: "direct_call".to_string(),
118 });
119 }
120 }
121 }
122
123 results.push(result);
124 }
125
126 (results, edges)
127 };
128
129 let total_chars: usize = symbol_results
131 .iter()
132 .map(|r| {
133 let sym_size = r
134 .symbol
135 .as_ref()
136 .map(|s| s.name.len() + s.signature.len())
137 .unwrap_or(0);
138 let source_size = r.source.as_ref().map(|s| s.len()).unwrap_or(0);
139 sym_size + source_size
140 })
141 .sum();
142 let mut estimated_tokens = (total_chars / 4) as u32;
143
144 let mut symbol_results = symbol_results;
146 if req.max_tokens > 0 && estimated_tokens > req.max_tokens {
147 let mut remaining = req.max_tokens;
148
149 for result in &mut symbol_results {
150 let sym_tokens = result
151 .symbol
152 .as_ref()
153 .map(|s| ((s.name.len() + s.signature.len()) / 4) as u32)
154 .unwrap_or(0);
155
156 if remaining < sym_tokens {
157 result.source = None;
159 continue;
160 }
161 remaining -= sym_tokens;
162
163 if let Some(ref source) = result.source {
164 let source_tokens = (source.len() / 4) as u32;
165 if remaining < source_tokens {
166 let max_chars = (remaining as usize) * 4;
167 result.source = Some(source[..max_chars.min(source.len())].to_string());
168 remaining = 0;
169 } else {
170 remaining -= source_tokens;
171 }
172 }
173 }
174
175 estimated_tokens = req.max_tokens - remaining;
176 }
177
178 info!(
179 session_id = %req.session_id,
180 query = %req.query,
181 results = symbol_results.len(),
182 estimated_tokens,
183 "CONTEXT: query served"
184 );
185
186 Ok(Response::new(ContextResponse {
187 symbols: symbol_results,
188 call_graph: call_edges,
189 dependencies: if req.include_dependencies {
190 let (repo_id, _git_repo) = engine
191 .get_repo(&session.codebase)
192 .await
193 .map_err(|e| Status::internal(format!("Repo error: {e}")))?;
194
195 let deps = engine
196 .dep_store()
197 .find_by_repo(repo_id)
198 .await
199 .unwrap_or_default();
200
201 let mut dep_refs = Vec::with_capacity(deps.len());
202 for dep in &deps {
203 let symbol_ids = engine
204 .dep_store()
205 .find_symbols_for_dep(dep.id)
206 .await
207 .unwrap_or_default();
208
209 dep_refs.push(crate::DependencyRef {
210 package: dep.package.clone(),
211 version_req: dep.version_req.clone(),
212 used_by_symbol_ids: symbol_ids.iter().map(|id| id.to_string()).collect(),
213 });
214 }
215 dep_refs
216 } else {
217 vec![]
218 },
219 estimated_tokens,
220 }))
221}
222
223fn symbol_to_ref(sym: &dk_core::Symbol) -> SymbolRef {
225 SymbolRef {
226 id: sym.id.to_string(),
227 name: sym.name.clone(),
228 qualified_name: sym.qualified_name.clone(),
229 kind: sym.kind.to_string(),
230 visibility: format!("{:?}", sym.visibility),
231 file_path: sym.file_path.to_string_lossy().to_string(),
232 start_byte: sym.span.start_byte,
233 end_byte: sym.span.end_byte,
234 signature: sym.signature.clone().unwrap_or_default(),
235 doc_comment: sym.doc_comment.clone(),
236 parent_id: sym.parent.map(|p| p.to_string()),
237 }
238}