use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Namespace {
File,
Symbol,
Project,
Session,
Agent,
Custom,
}
impl Namespace {
pub fn prefix(&self) -> &'static str {
match self {
Namespace::File => "file:",
Namespace::Symbol => "symbol:",
Namespace::Project => "project:",
Namespace::Session => "session:",
Namespace::Agent => "agent:",
Namespace::Custom => "custom:",
}
}
pub fn from_key(key: &str) -> (Self, &str) {
if let Some(rest) = key.strip_prefix("file:") {
(Namespace::File, rest)
} else if let Some(rest) = key.strip_prefix("symbol:") {
(Namespace::Symbol, rest)
} else if let Some(rest) = key.strip_prefix("project:") {
(Namespace::Project, rest)
} else if let Some(rest) = key.strip_prefix("session:") {
(Namespace::Session, rest)
} else if let Some(rest) = key.strip_prefix("agent:") {
(Namespace::Agent, rest)
} else if let Some(rest) = key.strip_prefix("custom:") {
(Namespace::Custom, rest)
} else {
(Namespace::Custom, key)
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContextEntry {
pub key: String,
pub value: serde_json::Value,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub git_commit: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_mtime: Option<i64>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metadata: HashMap<String, String>,
}
impl ContextEntry {
pub fn new(key: impl Into<String>, value: serde_json::Value) -> Self {
let now = Utc::now();
Self {
key: key.into(),
value,
created_at: now,
updated_at: now,
expires_at: None,
git_commit: None,
file_path: None,
file_mtime: None,
metadata: HashMap::new(),
}
}
pub fn with_ttl(mut self, ttl_secs: i64) -> Self {
self.expires_at = Some(Utc::now() + chrono::Duration::seconds(ttl_secs));
self
}
pub fn with_git_commit(mut self, commit: impl Into<String>) -> Self {
self.git_commit = Some(commit.into());
self
}
pub fn with_file_info(mut self, path: impl Into<String>, mtime: i64) -> Self {
self.file_path = Some(path.into());
self.file_mtime = Some(mtime);
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn is_expired(&self) -> bool {
if let Some(expires_at) = self.expires_at {
Utc::now() > expires_at
} else {
false
}
}
pub fn namespace(&self) -> Namespace {
Namespace::from_key(&self.key).0
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FileContext {
pub path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub symbols: Vec<SymbolInfo>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub imports: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub exports: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub line_count: Option<usize>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub metrics: HashMap<String, f64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub related_files: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SymbolInfo {
pub name: String,
pub kind: SymbolKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub line: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub visibility: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SymbolKind {
Function,
Method,
Class,
Struct,
Enum,
Interface,
Trait,
Type,
Constant,
Variable,
Module,
Other,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ProjectContext {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub languages: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub frameworks: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub architecture: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub directories: HashMap<String, String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub conventions: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub entry_points: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub test_pattern: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub build_commands: HashMap<String, String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SessionContext {
pub session_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub working_files: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub decisions: Vec<Decision>,
#[serde(skip_serializing_if = "Option::is_none")]
pub current_task: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub related_issues: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub learnings: Vec<String>,
pub started_at: DateTime<Utc>,
pub last_activity: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Decision {
pub decision: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub rationale: Option<String>,
pub timestamp: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub context: Vec<String>,
}
#[derive(Debug, Clone, Default)]
pub struct ContextQuery {
pub namespace: Option<Namespace>,
pub prefix: Option<String>,
pub include_expired: bool,
pub limit: Option<usize>,
pub offset: Option<usize>,
}
impl ContextQuery {
pub fn new() -> Self {
Self::default()
}
pub fn namespace(mut self, ns: Namespace) -> Self {
self.namespace = Some(ns);
self
}
pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
self.prefix = Some(prefix.into());
self
}
pub fn include_expired(mut self) -> Self {
self.include_expired = true;
self
}
pub fn limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
}