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}