1use chrono::{DateTime, Utc};
6#[cfg(feature = "json-schema")]
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use std::fmt;
10use std::path::PathBuf;
11
12#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
18#[serde(rename_all = "lowercase")]
19pub enum Kind {
20 Conversations,
21 Plans,
22 Reports,
23 #[default]
24 Other,
25}
26
27impl Kind {
28 pub fn dir_name(self) -> &'static str {
30 match self {
31 Self::Conversations => "conversations",
32 Self::Plans => "plans",
33 Self::Reports => "reports",
34 Self::Other => "other",
35 }
36 }
37
38 pub fn parse(s: &str) -> Option<Self> {
40 match s.to_ascii_lowercase().as_str() {
41 "conversations" | "conversation" => Some(Self::Conversations),
42 "plans" | "plan" => Some(Self::Plans),
43 "reports" | "report" => Some(Self::Reports),
44 "other" => Some(Self::Other),
45 _ => None,
46 }
47 }
48}
49
50impl fmt::Display for Kind {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 f.write_str(self.dir_name())
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
62#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
63#[serde(rename_all = "snake_case")]
64pub enum FrameKind {
65 UserMsg,
66 AgentReply,
67 InternalThought,
68 ToolCall,
69}
70
71impl FrameKind {
72 pub fn as_str(self) -> &'static str {
73 match self {
74 Self::UserMsg => "user_msg",
75 Self::AgentReply => "agent_reply",
76 Self::InternalThought => "internal_thought",
77 Self::ToolCall => "tool_call",
78 }
79 }
80
81 pub fn parse(value: &str) -> Option<Self> {
82 match value.trim().to_ascii_lowercase().as_str() {
83 "user_msg" | "user" => Some(Self::UserMsg),
84 "agent_reply" | "assistant" | "reply" => Some(Self::AgentReply),
85 "internal_thought" | "thought" | "thinking" | "reasoning" => {
86 Some(Self::InternalThought)
87 }
88 "tool_call" | "tool" | "tool_result" | "function_call" => Some(Self::ToolCall),
89 _ => None,
90 }
91 }
92}
93
94impl fmt::Display for FrameKind {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 f.write_str(self.as_str())
97 }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct TimelineEntry {
103 pub timestamp: DateTime<Utc>,
104 pub agent: String,
105 pub session_id: String,
106 pub role: String,
107 pub message: String,
108 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub frame_kind: Option<FrameKind>,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub branch: Option<String>,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub cwd: Option<String>,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct ConversationMessage {
124 pub timestamp: DateTime<Utc>,
125 pub agent: String,
126 pub session_id: String,
127 pub role: String,
129 pub message: String,
131 pub repo_project: String,
133 #[serde(skip_serializing_if = "Option::is_none")]
135 pub source_path: Option<String>,
136 #[serde(skip_serializing_if = "Option::is_none")]
138 pub branch: Option<String>,
139}
140
141#[derive(Debug, Clone)]
143pub struct ExtractionConfig {
144 pub project_filter: Vec<String>,
145 pub cutoff: DateTime<Utc>,
146 pub include_assistant: bool,
147 pub watermark: Option<DateTime<Utc>>,
148}
149
150#[derive(Debug, Clone, Serialize)]
152pub struct SourceInfo {
153 pub agent: String,
154 pub path: PathBuf,
155 pub sessions: usize,
156 pub size_bytes: u64,
157 pub protected_by_git: bool,
158 pub protection_backend: String,
159 pub protection_root: Option<PathBuf>,
160 pub git_remote_count: usize,
161 pub git_remotes: Vec<String>,
162 pub protection_warning: Option<String>,
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
172pub enum SourceTier {
173 Primary,
176 Secondary,
179 Fallback,
182 Opaque,
186}
187
188impl SourceTier {
189 pub fn is_assertable(self) -> bool {
192 matches!(self, Self::Primary | Self::Secondary)
193 }
194}
195
196#[derive(Debug, Clone, PartialEq, Eq, Hash)]
197pub struct RepoIdentity {
198 pub organization: String,
199 pub repository: String,
200}
201
202impl RepoIdentity {
203 pub fn slug(&self) -> String {
204 format!("{}/{}", self.organization, self.repository)
205 }
206}
207
208#[derive(Debug, Clone)]
209pub struct SemanticSegment {
210 pub repo: Option<RepoIdentity>,
211 pub source_tier: Option<SourceTier>,
214 pub kind: Kind,
215 pub agent: String,
216 pub session_id: String,
217 pub entries: Vec<TimelineEntry>,
218}
219
220impl SemanticSegment {
221 pub fn project_label(&self) -> String {
222 self.repo
223 .as_ref()
224 .map(RepoIdentity::slug)
225 .unwrap_or_else(|| "non-repository-contexts".to_string())
226 }
227
228 pub fn has_assertable_identity(&self) -> bool {
231 self.source_tier.is_some_and(SourceTier::is_assertable)
232 }
233}