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 SystemNote,
70}
71
72impl FrameKind {
73 pub fn as_str(self) -> &'static str {
74 match self {
75 Self::UserMsg => "user_msg",
76 Self::AgentReply => "agent_reply",
77 Self::InternalThought => "internal_thought",
78 Self::ToolCall => "tool_call",
79 Self::SystemNote => "system_note",
80 }
81 }
82
83 pub fn parse(value: &str) -> Option<Self> {
84 match value.trim().to_ascii_lowercase().as_str() {
85 "user_msg" | "user" => Some(Self::UserMsg),
86 "agent_reply" | "assistant" | "reply" => Some(Self::AgentReply),
87 "internal_thought" | "thought" | "thinking" | "reasoning" => {
88 Some(Self::InternalThought)
89 }
90 "tool_call" | "tool" | "tool_result" | "function_call" => Some(Self::ToolCall),
91 "system_note" | "system" | "note" | "notification" | "error" => Some(Self::SystemNote),
92 _ => None,
93 }
94 }
95}
96
97impl fmt::Display for FrameKind {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 f.write_str(self.as_str())
100 }
101}
102
103#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
108#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
109#[serde(rename_all = "snake_case")]
110pub enum MessageKind {
111 #[default]
112 Conversation,
113 WorkflowPrompt,
114 ContinuationSummary,
115 CollapseStub,
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
119#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
120#[serde(rename_all = "snake_case")]
121pub enum CollapseStubKind {
122 SkillRef,
123 DedupRef,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct TimelineEntry {
129 pub timestamp: DateTime<Utc>,
130 pub agent: String,
131 pub session_id: String,
132 pub role: String,
133 pub message: String,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub frame_kind: Option<FrameKind>,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub branch: Option<String>,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub cwd: Option<String>,
140 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub timestamp_source: Option<String>,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub source_path: Option<String>,
144 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub source_sha256: Option<String>,
146 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub source_line_span: Option<(u64, u64)>,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct ConversationMessage {
158 pub timestamp: DateTime<Utc>,
159 pub agent: String,
160 pub session_id: String,
161 pub role: String,
163 pub message: String,
165 pub repo_project: String,
167 #[serde(skip_serializing_if = "Option::is_none")]
169 pub source_path: Option<String>,
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub branch: Option<String>,
173 #[serde(default)]
175 pub message_kind: MessageKind,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub collapse_stub_kind: Option<CollapseStubKind>,
179}
180
181#[derive(Debug, Clone)]
183pub struct ExtractionConfig {
184 pub project_filter: Vec<String>,
185 pub cutoff: DateTime<Utc>,
186 pub include_assistant: bool,
187 pub watermark: Option<DateTime<Utc>>,
188}
189
190#[derive(Debug, Clone, Serialize)]
192pub struct SourceInfo {
193 pub agent: String,
194 pub path: PathBuf,
195 pub sessions: usize,
196 pub size_bytes: u64,
197 pub protected_by_git: bool,
198 pub protection_backend: String,
199 pub protection_root: Option<PathBuf>,
200 pub git_remote_count: usize,
201 pub git_remotes: Vec<String>,
202 pub protection_warning: Option<String>,
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
212pub enum SourceTier {
213 Primary,
216 Secondary,
219 Fallback,
222 Opaque,
226}
227
228impl SourceTier {
229 pub fn is_assertable(self) -> bool {
232 matches!(self, Self::Primary | Self::Secondary)
233 }
234}
235
236#[derive(Debug, Clone, PartialEq, Eq, Hash)]
237pub struct RepoIdentity {
238 pub organization: String,
239 pub repository: String,
240}
241
242impl RepoIdentity {
243 pub fn slug(&self) -> String {
244 format!("{}/{}", self.organization, self.repository)
245 }
246}
247
248#[derive(Debug, Clone)]
249pub struct SemanticSegment {
250 pub repo: Option<RepoIdentity>,
251 pub source_tier: Option<SourceTier>,
254 pub kind: Kind,
255 pub agent: String,
256 pub session_id: String,
257 pub entries: Vec<TimelineEntry>,
258}
259
260impl SemanticSegment {
261 pub fn project_label(&self) -> String {
262 self.repo
263 .as_ref()
264 .map(RepoIdentity::slug)
265 .unwrap_or_else(|| "non-repository-contexts".to_string())
266 }
267
268 pub fn has_assertable_identity(&self) -> bool {
271 self.source_tier.is_some_and(SourceTier::is_assertable)
272 }
273}