use chrono::{DateTime, Utc};
#[cfg(feature = "json-schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Kind {
Conversations,
Plans,
Reports,
#[default]
Other,
}
impl Kind {
pub fn dir_name(self) -> &'static str {
match self {
Self::Conversations => "conversations",
Self::Plans => "plans",
Self::Reports => "reports",
Self::Other => "other",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"conversations" | "conversation" => Some(Self::Conversations),
"plans" | "plan" => Some(Self::Plans),
"reports" | "report" => Some(Self::Reports),
"other" => Some(Self::Other),
_ => None,
}
}
}
impl fmt::Display for Kind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.dir_name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum FrameKind {
UserMsg,
AgentReply,
InternalThought,
ToolCall,
}
impl FrameKind {
pub fn as_str(self) -> &'static str {
match self {
Self::UserMsg => "user_msg",
Self::AgentReply => "agent_reply",
Self::InternalThought => "internal_thought",
Self::ToolCall => "tool_call",
}
}
pub fn parse(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"user_msg" | "user" => Some(Self::UserMsg),
"agent_reply" | "assistant" | "reply" => Some(Self::AgentReply),
"internal_thought" | "thought" | "thinking" | "reasoning" => {
Some(Self::InternalThought)
}
"tool_call" | "tool" | "tool_result" | "function_call" => Some(Self::ToolCall),
_ => None,
}
}
}
impl fmt::Display for FrameKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimelineEntry {
pub timestamp: DateTime<Utc>,
pub agent: String,
pub session_id: String,
pub role: String,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub frame_kind: Option<FrameKind>,
#[serde(skip_serializing_if = "Option::is_none")]
pub branch: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cwd: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConversationMessage {
pub timestamp: DateTime<Utc>,
pub agent: String,
pub session_id: String,
pub role: String,
pub message: String,
pub repo_project: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub branch: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ExtractionConfig {
pub project_filter: Vec<String>,
pub cutoff: DateTime<Utc>,
pub include_assistant: bool,
pub watermark: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize)]
pub struct SourceInfo {
pub agent: String,
pub path: PathBuf,
pub sessions: usize,
pub size_bytes: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub enum SourceTier {
Primary,
Secondary,
Fallback,
Opaque,
}
impl SourceTier {
pub fn is_assertable(self) -> bool {
matches!(self, Self::Primary | Self::Secondary)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RepoIdentity {
pub organization: String,
pub repository: String,
}
impl RepoIdentity {
pub fn slug(&self) -> String {
format!("{}/{}", self.organization, self.repository)
}
}
#[derive(Debug, Clone)]
pub struct SemanticSegment {
pub repo: Option<RepoIdentity>,
pub source_tier: Option<SourceTier>,
pub kind: Kind,
pub agent: String,
pub session_id: String,
pub entries: Vec<TimelineEntry>,
}
impl SemanticSegment {
pub fn project_label(&self) -> String {
self.repo
.as_ref()
.map(RepoIdentity::slug)
.unwrap_or_else(|| "non-repository-contexts".to_string())
}
pub fn has_assertable_identity(&self) -> bool {
self.source_tier.is_some_and(SourceTier::is_assertable)
}
}