use agtrace_types::{AgentEvent, ToolCallPayload, ToolKind, ToolOrigin};
use serde_json::Value;
use std::path::{Path, PathBuf};
use crate::{Error, Result};
pub trait LogDiscovery: Send + Sync {
fn id(&self) -> &'static str;
fn probe(&self, path: &Path) -> ProbeResult;
fn resolve_log_root(&self, project_root: &Path) -> Option<PathBuf>;
fn scan_sessions(&self, log_root: &Path) -> Result<Vec<SessionIndex>>;
fn extract_session_id(&self, path: &Path) -> Result<String>;
fn extract_project_hash(&self, path: &Path) -> Result<Option<agtrace_types::ProjectHash>>;
fn find_session_files(&self, log_root: &Path, session_id: &str) -> Result<Vec<PathBuf>>;
fn is_sidechain_file(&self, path: &Path) -> Result<bool>;
}
pub trait SessionParser: Send + Sync {
fn parse_file(&self, path: &Path) -> Result<Vec<AgentEvent>>;
fn parse_record(&self, content: &str) -> Result<Option<AgentEvent>>;
}
pub trait ToolMapper: Send + Sync {
fn classify(&self, tool_name: &str) -> (ToolOrigin, ToolKind);
fn normalize_call(&self, name: &str, args: Value, call_id: Option<String>) -> ToolCallPayload;
fn summarize(&self, kind: ToolKind, args: &Value) -> String;
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ProbeResult {
Confidence(f32),
NoMatch,
}
impl ProbeResult {
pub fn match_high() -> Self {
ProbeResult::Confidence(1.0)
}
pub fn match_medium() -> Self {
ProbeResult::Confidence(0.5)
}
pub fn match_low() -> Self {
ProbeResult::Confidence(0.3)
}
pub fn is_match(&self) -> bool {
matches!(self, ProbeResult::Confidence(c) if *c > 0.0)
}
pub fn confidence(&self) -> f32 {
match self {
ProbeResult::Confidence(c) => *c,
ProbeResult::NoMatch => 0.0,
}
}
}
#[derive(Debug, Clone)]
pub struct SessionIndex {
pub session_id: String,
pub timestamp: Option<String>,
pub latest_mod_time: Option<String>,
pub main_file: PathBuf,
pub sidechain_files: Vec<PathBuf>,
pub project_root: Option<PathBuf>,
pub repository_hash: Option<agtrace_types::RepositoryHash>,
pub snippet: Option<String>,
pub parent_session_id: Option<String>,
pub spawned_by: Option<agtrace_types::SpawnContext>,
}
pub struct ProviderAdapter {
pub discovery: Box<dyn LogDiscovery>,
pub parser: Box<dyn SessionParser>,
pub mapper: Box<dyn ToolMapper>,
}
impl ProviderAdapter {
pub fn new(
discovery: Box<dyn LogDiscovery>,
parser: Box<dyn SessionParser>,
mapper: Box<dyn ToolMapper>,
) -> Self {
Self {
discovery,
parser,
mapper,
}
}
pub fn from_name(provider_name: &str) -> Result<Self> {
match provider_name {
"claude_code" | "claude" => Ok(Self::claude()),
"codex" => Ok(Self::codex()),
"gemini" => Ok(Self::gemini()),
_ => Err(Error::Provider(format!(
"Unknown provider: {}",
provider_name
))),
}
}
pub fn claude() -> Self {
Self::new(
Box::new(crate::claude::ClaudeDiscovery),
Box::new(crate::claude::ClaudeParser),
Box::new(crate::claude::ClaudeToolMapper),
)
}
pub fn codex() -> Self {
Self::new(
Box::new(crate::codex::CodexDiscovery),
Box::new(crate::codex::CodexParser),
Box::new(crate::codex::CodexToolMapper),
)
}
pub fn gemini() -> Self {
Self::new(
Box::new(crate::gemini::GeminiDiscovery),
Box::new(crate::gemini::GeminiParser),
Box::new(crate::gemini::GeminiToolMapper),
)
}
pub fn id(&self) -> &'static str {
self.discovery.id()
}
pub fn process_file(&self, path: &Path) -> Result<Vec<AgentEvent>> {
if !self.discovery.probe(path).is_match() {
return Err(Error::Provider(format!(
"Provider {} cannot handle file: {}",
self.id(),
path.display()
)));
}
self.parser.parse_file(path)
}
}
pub fn get_latest_mod_time_rfc3339(files: &[&std::path::Path]) -> Option<String> {
use chrono::{DateTime, Utc};
use std::time::SystemTime;
let mut latest: Option<SystemTime> = None;
for path in files {
if let Ok(metadata) = std::fs::metadata(path)
&& let Ok(modified) = metadata.modified()
&& (latest.is_none() || Some(modified) > latest)
{
latest = Some(modified);
}
}
latest.map(|t| {
let dt: DateTime<Utc> = t.into();
dt.to_rfc3339()
})
}