Skip to main content

agtrace_types/domain/
session.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5use super::project::ProjectHash;
6use super::token_usage::ContextWindowUsage;
7use crate::{
8    MessagePayload, ReasoningPayload, SlashCommandPayload, StreamId, ToolCallPayload,
9    ToolResultPayload, UserPayload,
10};
11
12/// Source of the agent log (provider-agnostic identifier)
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(transparent)]
15pub struct Source(String);
16
17impl Source {
18    pub fn new(name: impl Into<String>) -> Self {
19        Self(name.into())
20    }
21}
22
23/// Subagent execution information
24///
25/// Represents metadata about subagent (agent-within-agent) execution.
26/// Different providers implement subagents differently:
27/// - Claude Code: Uses Task tool with `subagent_type` and returns `agentId`
28/// - Codex: Creates separate session files with `source.subagent` metadata
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct SubagentInfo {
31    /// Subagent identifier (e.g., "ba2ed465" for Claude Code, session ID for Codex)
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub agent_id: Option<String>,
34
35    /// Subagent type/role (e.g., "Explore", "general-purpose", "review")
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub agent_type: Option<String>,
38
39    /// Parent session ID (for Codex where subagent is a separate session)
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub parent_session_id: Option<String>,
42}
43
44/// Tool execution status (used in Span API)
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(rename_all = "snake_case")]
47pub enum ToolStatus {
48    Success,
49    Error,
50    InProgress,
51    Unknown,
52}
53
54/// Order for session listing
55#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "snake_case")]
57pub enum SessionOrder {
58    /// Most recent first (end_ts DESC, start_ts DESC)
59    #[default]
60    NewestFirst,
61    /// Oldest first (start_ts ASC, end_ts ASC)
62    OldestFirst,
63}
64
65/// Session summary for listing
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct SessionSummary {
68    pub session_id: String,
69    pub source: Source,
70    pub project_hash: ProjectHash,
71    pub start_ts: String,
72    pub end_ts: String,
73    pub event_count: usize,
74    pub user_message_count: usize,
75    pub tokens_input_total: u64,
76    pub tokens_output_total: u64,
77}
78
79/// Session metadata (DB-derived, not available from events alone).
80///
81/// Contains information inferred from filesystem paths and stored in the index.
82/// Separate from AgentSession which is assembled purely from events.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct SessionMetadata {
85    /// Unique session identifier.
86    pub session_id: String,
87    /// Project hash (inferred from log file path).
88    pub project_hash: ProjectHash,
89    /// Project root path (resolved from project_hash).
90    pub project_root: Option<String>,
91    /// Provider name (claude_code, codex, gemini).
92    pub provider: String,
93    /// Parent session ID for subagent sessions.
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub parent_session_id: Option<String>,
96    /// Spawn context for subagent sessions (turn/step where spawned).
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub spawned_by: Option<SpawnContext>,
99}
100
101// ==========================================
102// 1. Session (entire conversation)
103// ==========================================
104
105/// Context about how a sidechain was spawned from a parent session.
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct SpawnContext {
108    /// Turn index (0-based) in the parent session where this sidechain was spawned.
109    pub turn_index: usize,
110    /// Step index (0-based) within the turn where the Task tool was called.
111    pub step_index: usize,
112}
113
114/// Complete agent conversation session assembled from normalized events.
115///
116/// Represents a full conversation timeline with the agent, containing all
117/// user interactions (turns) and their corresponding agent responses.
118/// The session is the highest-level construct for analyzing agent behavior.
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct AgentSession {
121    /// Unique session identifier.
122    pub session_id: Uuid,
123    /// Stream identifier for multi-stream sessions.
124    /// Indicates whether this is the main conversation, a sidechain, or a subagent.
125    pub stream_id: StreamId,
126    /// For sidechain sessions: context about where this was spawned from in the parent session.
127    /// None for main stream sessions or sidechains without identifiable parent context.
128    #[serde(default, skip_serializing_if = "Option::is_none")]
129    pub spawned_by: Option<SpawnContext>,
130    /// When the session started (first event timestamp).
131    pub start_time: DateTime<Utc>,
132    /// When the session ended (last event timestamp), if completed.
133    pub end_time: Option<DateTime<Utc>>,
134
135    /// All user-initiated turns in chronological order.
136    pub turns: Vec<AgentTurn>,
137
138    /// Aggregated session statistics.
139    pub stats: SessionStats,
140}
141
142// ==========================================
143// 2. Turn (user-initiated interaction unit)
144// ==========================================
145
146/// Single user-initiated interaction cycle within a session.
147///
148/// A turn begins with user input and contains all agent steps taken
149/// in response until the next user input or session end.
150/// Autonomous agents may execute multiple steps per turn.
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct AgentTurn {
153    /// Unique turn identifier (ID of the user event that initiated this turn).
154    pub id: Uuid,
155    /// When the turn started (user input timestamp).
156    pub timestamp: DateTime<Utc>,
157
158    /// User input that triggered this turn.
159    pub user: UserMessage,
160
161    /// Agent's response steps in chronological order.
162    /// Single step for simple Q&A, multiple steps for autonomous operation.
163    pub steps: Vec<AgentStep>,
164
165    /// Aggregated turn statistics.
166    pub stats: TurnStats,
167}
168
169// ==========================================
170// 3. Step (single LLM inference + execution unit)
171// ==========================================
172
173/// Single LLM inference cycle with optional tool executions.
174///
175/// A step represents one round of agent thinking and acting:
176/// 1. Generation phase: LLM produces reasoning, messages, and tool calls
177/// 2. Execution phase: Tools are executed and results collected
178///
179/// Steps are the atomic unit of agent behavior analysis.
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct AgentStep {
182    /// Unique step identifier (ID of the first event in this step).
183    pub id: Uuid,
184    /// When the step started.
185    pub timestamp: DateTime<Utc>,
186
187    // --- Phase 1: Generation (Agent Outputs) ---
188    // These are generated in parallel or in arbitrary order before seeing tool results
189    /// Chain-of-thought reasoning, if present.
190    pub reasoning: Option<ReasoningBlock>,
191
192    /// Text response to user or explanation of tool usage.
193    pub message: Option<MessageBlock>,
194
195    // --- Phase 2: Execution (System Outputs) ---
196    /// Tool executions (call + result pairs) performed in this step.
197    /// Calls are generated in Phase 1, paired with results here.
198    pub tools: Vec<ToolExecution>,
199
200    // --- Meta ---
201    /// Token usage for this step's LLM inference, if available.
202    pub usage: Option<ContextWindowUsage>,
203    /// Whether this step encountered any failures.
204    pub is_failed: bool,
205    /// Current completion status of this step.
206    pub status: StepStatus,
207}
208
209/// Step completion status
210#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
211#[serde(rename_all = "snake_case")]
212pub enum StepStatus {
213    /// Step completed successfully (has Message or all tools have results)
214    Done,
215    /// Step is waiting for tool results or next action
216    InProgress,
217    /// Step failed with errors
218    Failed,
219}
220
221// ==========================================
222// Components
223// ==========================================
224
225/// Paired tool call and result with execution metrics.
226///
227/// Represents a complete tool execution lifecycle:
228/// tool invocation → execution → result collection.
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct ToolExecution {
231    /// Tool invocation request.
232    pub call: ToolCallBlock,
233
234    /// Execution result (None if incomplete, lost, or still pending).
235    pub result: Option<ToolResultBlock>,
236
237    /// Execution latency in milliseconds (result.timestamp - call.timestamp).
238    pub duration_ms: Option<i64>,
239
240    /// Whether this tool execution failed (error status in result).
241    pub is_error: bool,
242}
243
244// --- Event Wrappers ---
245
246/// Origin of a user message in a turn.
247///
248/// Distinguishes user-typed input from system-generated messages.
249/// Orthogonal to `slash_command` - a turn can have both origin and slash_command.
250#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
251#[serde(tag = "type", rename_all = "snake_case")]
252pub enum TurnOrigin {
253    /// User-typed input (default, human-initiated).
254    #[default]
255    User,
256    /// System-generated input (not typed by user).
257    SystemGenerated {
258        /// Reason for system generation.
259        reason: SystemGeneratedReason,
260    },
261}
262
263/// Reason for system-generated turns.
264#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
265#[serde(rename_all = "snake_case")]
266pub enum SystemGeneratedReason {
267    /// Context compaction/auto-summarization continuation.
268    ContextCompaction,
269    /// Other/unknown system-generated reason.
270    Other,
271}
272
273impl TurnOrigin {
274    /// Check if this is a system-generated turn.
275    pub fn is_system_generated(&self) -> bool {
276        matches!(self, TurnOrigin::SystemGenerated { .. })
277    }
278
279    /// Check if this is a context compaction turn.
280    pub fn is_context_compaction(&self) -> bool {
281        matches!(
282            self,
283            TurnOrigin::SystemGenerated {
284                reason: SystemGeneratedReason::ContextCompaction
285            }
286        )
287    }
288}
289
290/// User input message that initiates a turn.
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct UserMessage {
293    /// ID of the source event.
294    pub event_id: Uuid,
295    /// User input content (empty if triggered by slash command).
296    pub content: UserPayload,
297    /// Slash command that triggered this turn (e.g., /commit, /skaffold-repo).
298    #[serde(default, skip_serializing_if = "Option::is_none")]
299    pub slash_command: Option<SlashCommandPayload>,
300    /// Origin of this message (user-typed vs system-generated).
301    #[serde(default)]
302    pub origin: TurnOrigin,
303}
304
305/// Agent reasoning/thinking block.
306#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct ReasoningBlock {
308    /// ID of the source event.
309    pub event_id: Uuid,
310    /// Reasoning content.
311    pub content: ReasoningPayload,
312}
313
314/// Agent text response message.
315#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct MessageBlock {
317    /// ID of the source event.
318    pub event_id: Uuid,
319    /// Message content.
320    pub content: MessagePayload,
321}
322
323/// Tool invocation request with timing information.
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct ToolCallBlock {
326    /// ID of the source event.
327    pub event_id: Uuid,
328    /// When the tool was invoked.
329    pub timestamp: DateTime<Utc>,
330    /// Provider-specific call identifier, if available.
331    pub provider_call_id: Option<String>,
332    /// Tool invocation details.
333    pub content: ToolCallPayload,
334}
335
336/// Tool execution result with timing information.
337#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct ToolResultBlock {
339    /// ID of the source event.
340    pub event_id: Uuid,
341    /// When the result was received.
342    pub timestamp: DateTime<Utc>,
343    /// ID of the tool call this result corresponds to.
344    pub tool_call_id: Uuid,
345    /// Tool execution result details.
346    pub content: ToolResultPayload,
347}
348
349// --- Stats ---
350
351/// Aggregated statistics for an entire session.
352#[derive(Debug, Clone, Serialize, Deserialize, Default)]
353pub struct SessionStats {
354    /// Total number of turns in the session.
355    pub total_turns: usize,
356    /// Session duration in seconds (end_time - start_time).
357    pub duration_seconds: i64,
358    /// Total tokens consumed across all turns.
359    pub total_tokens: i64,
360}
361
362/// Aggregated statistics for a single turn.
363#[derive(Debug, Clone, Serialize, Deserialize, Default)]
364pub struct TurnStats {
365    /// Turn duration in milliseconds.
366    pub duration_ms: i64,
367    /// Number of steps in this turn.
368    pub step_count: usize,
369    /// Total tokens consumed in this turn.
370    pub total_tokens: i32,
371}
372
373// ==========================================
374// Computed Metrics (for presentation)
375// ==========================================
376
377/// Computed context window metrics for turn visualization.
378///
379/// Used by TUI and other presentation layers to show
380/// cumulative token usage and detect high-usage patterns.
381#[derive(Debug, Clone)]
382pub struct TurnMetrics {
383    /// Zero-based turn index.
384    pub turn_index: usize,
385    /// Cumulative tokens before this turn (0 if context was compacted).
386    pub prev_total: u32,
387    /// Tokens added by this turn (or new baseline if compacted).
388    pub delta: u32,
389    /// Whether this turn's delta exceeds the heavy threshold.
390    pub is_heavy: bool,
391    /// Whether this turn is currently active (in progress).
392    pub is_active: bool,
393    /// True if context was compacted (reset) during this turn.
394    pub context_compacted: bool,
395    /// Actual cumulative tokens at end of this turn.
396    pub cumulative_total: u32,
397    /// Previous cumulative tokens before compaction (only set when context_compacted is true).
398    /// Used to visualize the reduction (e.g., 150k → 30k).
399    pub compaction_from: Option<u32>,
400}
401
402impl TurnMetrics {
403    /// Calculate heavy threshold: 10% of max context, or fallback to 15k tokens
404    pub fn heavy_threshold(max_context: Option<u32>) -> u32 {
405        max_context.map(|mc| mc / 10).unwrap_or(15000)
406    }
407
408    /// Check if a delta is considered heavy
409    pub fn is_delta_heavy(delta: u32, max_context: Option<u32>) -> bool {
410        delta >= Self::heavy_threshold(max_context)
411    }
412}
413
414impl AgentTurn {
415    /// Calculate cumulative input tokens at the end of this turn
416    /// Falls back to `fallback` if no usage data found
417    pub fn cumulative_input_tokens(&self, fallback: u32) -> u32 {
418        self.steps
419            .iter()
420            .rev()
421            .find_map(|step| step.usage.as_ref())
422            .map(|usage| usage.input_tokens() as u32)
423            .unwrap_or(fallback)
424    }
425
426    /// Calculate cumulative total tokens (input + output) at the end of this turn
427    /// Falls back to `fallback` if no usage data found
428    pub fn cumulative_total_tokens(&self, fallback: u32) -> u32 {
429        self.steps
430            .iter()
431            .rev()
432            .find_map(|step| step.usage.as_ref())
433            .map(|usage| (usage.input_tokens() + usage.output_tokens()) as u32)
434            .unwrap_or(fallback)
435    }
436
437    /// Check if this turn is currently active
438    ///
439    /// A turn is active if any of the recent steps are in progress.
440    /// Looking at multiple steps provides stability during step transitions
441    /// (e.g., when a step completes but the next one hasn't started yet).
442    pub fn is_active(&self) -> bool {
443        const LOOKBACK_STEPS: usize = 3;
444
445        self.steps
446            .iter()
447            .rev()
448            .take(LOOKBACK_STEPS)
449            .any(|step| matches!(step.status, StepStatus::InProgress))
450    }
451}