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}