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}