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