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