1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum Namespace {
11 File,
13 Symbol,
15 Project,
17 Session,
19 Agent,
21 Custom,
23}
24
25impl Namespace {
26 pub fn prefix(&self) -> &'static str {
28 match self {
29 Namespace::File => "file:",
30 Namespace::Symbol => "symbol:",
31 Namespace::Project => "project:",
32 Namespace::Session => "session:",
33 Namespace::Agent => "agent:",
34 Namespace::Custom => "custom:",
35 }
36 }
37
38 pub fn from_key(key: &str) -> (Self, &str) {
40 if let Some(rest) = key.strip_prefix("file:") {
41 (Namespace::File, rest)
42 } else if let Some(rest) = key.strip_prefix("symbol:") {
43 (Namespace::Symbol, rest)
44 } else if let Some(rest) = key.strip_prefix("project:") {
45 (Namespace::Project, rest)
46 } else if let Some(rest) = key.strip_prefix("session:") {
47 (Namespace::Session, rest)
48 } else if let Some(rest) = key.strip_prefix("agent:") {
49 (Namespace::Agent, rest)
50 } else if let Some(rest) = key.strip_prefix("custom:") {
51 (Namespace::Custom, rest)
52 } else {
53 (Namespace::Custom, key)
54 }
55 }
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ContextEntry {
61 pub key: String,
63
64 pub value: serde_json::Value,
66
67 pub created_at: DateTime<Utc>,
69
70 pub updated_at: DateTime<Utc>,
72
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub expires_at: Option<DateTime<Utc>>,
76
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub git_commit: Option<String>,
80
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub file_path: Option<String>,
84
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub file_mtime: Option<i64>,
88
89 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
91 pub metadata: HashMap<String, String>,
92}
93
94impl ContextEntry {
95 pub fn new(key: impl Into<String>, value: serde_json::Value) -> Self {
97 let now = Utc::now();
98 Self {
99 key: key.into(),
100 value,
101 created_at: now,
102 updated_at: now,
103 expires_at: None,
104 git_commit: None,
105 file_path: None,
106 file_mtime: None,
107 metadata: HashMap::new(),
108 }
109 }
110
111 pub fn with_ttl(mut self, ttl_secs: i64) -> Self {
113 self.expires_at = Some(Utc::now() + chrono::Duration::seconds(ttl_secs));
114 self
115 }
116
117 pub fn with_git_commit(mut self, commit: impl Into<String>) -> Self {
119 self.git_commit = Some(commit.into());
120 self
121 }
122
123 pub fn with_file_info(mut self, path: impl Into<String>, mtime: i64) -> Self {
125 self.file_path = Some(path.into());
126 self.file_mtime = Some(mtime);
127 self
128 }
129
130 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
132 self.metadata.insert(key.into(), value.into());
133 self
134 }
135
136 pub fn is_expired(&self) -> bool {
138 if let Some(expires_at) = self.expires_at {
139 Utc::now() > expires_at
140 } else {
141 false
142 }
143 }
144
145 pub fn namespace(&self) -> Namespace {
147 Namespace::from_key(&self.key).0
148 }
149}
150
151#[derive(Debug, Clone, Default, Serialize, Deserialize)]
153pub struct FileContext {
154 pub path: String,
156
157 #[serde(skip_serializing_if = "Option::is_none")]
159 pub summary: Option<String>,
160
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub language: Option<String>,
164
165 #[serde(default, skip_serializing_if = "Vec::is_empty")]
167 pub symbols: Vec<SymbolInfo>,
168
169 #[serde(default, skip_serializing_if = "Vec::is_empty")]
171 pub imports: Vec<String>,
172
173 #[serde(default, skip_serializing_if = "Vec::is_empty")]
175 pub exports: Vec<String>,
176
177 #[serde(skip_serializing_if = "Option::is_none")]
179 pub line_count: Option<usize>,
180
181 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
183 pub metrics: HashMap<String, f64>,
184
185 #[serde(default, skip_serializing_if = "Vec::is_empty")]
187 pub related_files: Vec<String>,
188
189 #[serde(default, skip_serializing_if = "Vec::is_empty")]
191 pub tags: Vec<String>,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct SymbolInfo {
197 pub name: String,
199
200 pub kind: SymbolKind,
202
203 #[serde(skip_serializing_if = "Option::is_none")]
205 pub line: Option<usize>,
206
207 #[serde(skip_serializing_if = "Option::is_none")]
209 pub description: Option<String>,
210
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub signature: Option<String>,
214
215 #[serde(skip_serializing_if = "Option::is_none")]
217 pub visibility: Option<String>,
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
222#[serde(rename_all = "snake_case")]
223pub enum SymbolKind {
224 Function,
225 Method,
226 Class,
227 Struct,
228 Enum,
229 Interface,
230 Trait,
231 Type,
232 Constant,
233 Variable,
234 Module,
235 Other,
236}
237
238#[derive(Debug, Clone, Default, Serialize, Deserialize)]
240pub struct ProjectContext {
241 #[serde(skip_serializing_if = "Option::is_none")]
243 pub name: Option<String>,
244
245 #[serde(skip_serializing_if = "Option::is_none")]
247 pub description: Option<String>,
248
249 #[serde(default, skip_serializing_if = "Vec::is_empty")]
251 pub languages: Vec<String>,
252
253 #[serde(default, skip_serializing_if = "Vec::is_empty")]
255 pub frameworks: Vec<String>,
256
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub architecture: Option<String>,
260
261 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
263 pub directories: HashMap<String, String>,
264
265 #[serde(default, skip_serializing_if = "Vec::is_empty")]
267 pub conventions: Vec<String>,
268
269 #[serde(default, skip_serializing_if = "Vec::is_empty")]
271 pub entry_points: Vec<String>,
272
273 #[serde(skip_serializing_if = "Option::is_none")]
275 pub test_pattern: Option<String>,
276
277 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
279 pub build_commands: HashMap<String, String>,
280}
281
282#[derive(Debug, Clone, Default, Serialize, Deserialize)]
284pub struct SessionContext {
285 pub session_id: String,
287
288 #[serde(skip_serializing_if = "Option::is_none")]
290 pub agent_id: Option<String>,
291
292 #[serde(default, skip_serializing_if = "Vec::is_empty")]
294 pub working_files: Vec<String>,
295
296 #[serde(default, skip_serializing_if = "Vec::is_empty")]
298 pub decisions: Vec<Decision>,
299
300 #[serde(skip_serializing_if = "Option::is_none")]
302 pub current_task: Option<String>,
303
304 #[serde(default, skip_serializing_if = "Vec::is_empty")]
306 pub related_issues: Vec<String>,
307
308 #[serde(default, skip_serializing_if = "Vec::is_empty")]
310 pub learnings: Vec<String>,
311
312 pub started_at: DateTime<Utc>,
314
315 pub last_activity: DateTime<Utc>,
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct Decision {
322 pub decision: String,
324
325 #[serde(skip_serializing_if = "Option::is_none")]
327 pub rationale: Option<String>,
328
329 pub timestamp: DateTime<Utc>,
331
332 #[serde(default, skip_serializing_if = "Vec::is_empty")]
334 pub context: Vec<String>,
335}
336
337#[derive(Debug, Clone, Default)]
339pub struct ContextQuery {
340 pub namespace: Option<Namespace>,
342
343 pub prefix: Option<String>,
345
346 pub include_expired: bool,
348
349 pub limit: Option<usize>,
351
352 pub offset: Option<usize>,
354}
355
356impl ContextQuery {
357 pub fn new() -> Self {
359 Self::default()
360 }
361
362 pub fn namespace(mut self, ns: Namespace) -> Self {
364 self.namespace = Some(ns);
365 self
366 }
367
368 pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
370 self.prefix = Some(prefix.into());
371 self
372 }
373
374 pub fn include_expired(mut self) -> Self {
376 self.include_expired = true;
377 self
378 }
379
380 pub fn limit(mut self, limit: usize) -> Self {
382 self.limit = Some(limit);
383 self
384 }
385}