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}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
166pub enum SourceTier {
167 Primary,
170 Secondary,
173 Fallback,
176 Opaque,
180}
181
182impl SourceTier {
183 pub fn is_assertable(self) -> bool {
186 matches!(self, Self::Primary | Self::Secondary)
187 }
188}
189
190#[derive(Debug, Clone, PartialEq, Eq, Hash)]
191pub struct RepoIdentity {
192 pub organization: String,
193 pub repository: String,
194}
195
196impl RepoIdentity {
197 pub fn slug(&self) -> String {
198 format!("{}/{}", self.organization, self.repository)
199 }
200}
201
202#[derive(Debug, Clone)]
203pub struct SemanticSegment {
204 pub repo: Option<RepoIdentity>,
205 pub source_tier: Option<SourceTier>,
208 pub kind: Kind,
209 pub agent: String,
210 pub session_id: String,
211 pub entries: Vec<TimelineEntry>,
212}
213
214impl SemanticSegment {
215 pub fn project_label(&self) -> String {
216 self.repo
217 .as_ref()
218 .map(RepoIdentity::slug)
219 .unwrap_or_else(|| "non-repository-contexts".to_string())
220 }
221
222 pub fn has_assertable_identity(&self) -> bool {
225 self.source_tier.is_some_and(SourceTier::is_assertable)
226 }
227}