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}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct ConversationMessage {
152 pub timestamp: DateTime<Utc>,
153 pub agent: String,
154 pub session_id: String,
155 pub role: String,
157 pub message: String,
159 pub repo_project: String,
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub source_path: Option<String>,
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub branch: Option<String>,
167 #[serde(default)]
169 pub message_kind: MessageKind,
170 #[serde(default, skip_serializing_if = "Option::is_none")]
172 pub collapse_stub_kind: Option<CollapseStubKind>,
173}
174
175#[derive(Debug, Clone)]
177pub struct ExtractionConfig {
178 pub project_filter: Vec<String>,
179 pub cutoff: DateTime<Utc>,
180 pub include_assistant: bool,
181 pub watermark: Option<DateTime<Utc>>,
182}
183
184#[derive(Debug, Clone, Serialize)]
186pub struct SourceInfo {
187 pub agent: String,
188 pub path: PathBuf,
189 pub sessions: usize,
190 pub size_bytes: u64,
191 pub protected_by_git: bool,
192 pub protection_backend: String,
193 pub protection_root: Option<PathBuf>,
194 pub git_remote_count: usize,
195 pub git_remotes: Vec<String>,
196 pub protection_warning: Option<String>,
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
206pub enum SourceTier {
207 Primary,
210 Secondary,
213 Fallback,
216 Opaque,
220}
221
222impl SourceTier {
223 pub fn is_assertable(self) -> bool {
226 matches!(self, Self::Primary | Self::Secondary)
227 }
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Hash)]
231pub struct RepoIdentity {
232 pub organization: String,
233 pub repository: String,
234}
235
236impl RepoIdentity {
237 pub fn slug(&self) -> String {
238 format!("{}/{}", self.organization, self.repository)
239 }
240}
241
242#[derive(Debug, Clone)]
243pub struct SemanticSegment {
244 pub repo: Option<RepoIdentity>,
245 pub source_tier: Option<SourceTier>,
248 pub kind: Kind,
249 pub agent: String,
250 pub session_id: String,
251 pub entries: Vec<TimelineEntry>,
252}
253
254impl SemanticSegment {
255 pub fn project_label(&self) -> String {
256 self.repo
257 .as_ref()
258 .map(RepoIdentity::slug)
259 .unwrap_or_else(|| "non-repository-contexts".to_string())
260 }
261
262 pub fn has_assertable_identity(&self) -> bool {
265 self.source_tier.is_some_and(SourceTier::is_assertable)
266 }
267}