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