agtrace_types/event/
payload.rs

1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4use crate::tool::ToolCallPayload;
5
6/// Event payload variants
7#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(tag = "type", content = "content")]
9#[serde(rename_all = "snake_case")]
10pub enum EventPayload {
11    /// 1. User input (Trigger)
12    User(UserPayload),
13
14    /// 2. Assistant reasoning/thinking process (Gemini thoughts, etc.)
15    Reasoning(ReasoningPayload),
16
17    /// 3. Tool execution request (Action Request)
18    ///
19    /// Note: TokenUsage can be attached as sidecar to this
20    ToolCall(ToolCallPayload),
21
22    /// 4. Tool execution result (Action Result)
23    ToolResult(ToolResultPayload),
24
25    /// 5. Assistant text response (Final Response)
26    ///
27    /// Note: TokenUsage can be attached as sidecar to this
28    Message(MessagePayload),
29
30    /// 6. Cost information (Sidecar / Leaf Node)
31    ///
32    /// Not included in context, used for cost calculation
33    TokenUsage(TokenUsagePayload),
34
35    /// 7. User-facing system notification (updates, alerts, status changes)
36    Notification(NotificationPayload),
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct UserPayload {
41    /// User input text
42    pub text: String,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ReasoningPayload {
47    /// Reasoning/thinking content
48    pub text: String,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct ToolResultPayload {
53    /// Tool execution result (text, JSON string, error message, etc.)
54    pub output: String,
55
56    /// Logical parent (Tool Call) reference ID
57    /// Separate from parent_id (time-series parent) to explicitly identify which call this result belongs to
58    pub tool_call_id: Uuid,
59
60    /// Execution success or failure
61    #[serde(default)]
62    pub is_error: bool,
63
64    /// Agent ID if this result spawned a subagent (e.g., "be466c0a")
65    /// Used to link sidechain sessions back to their parent turn/step
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub agent_id: Option<String>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct MessagePayload {
72    /// Response text
73    pub text: String,
74}
75
76// ============================================================================
77// Token Usage Normalization
78// ============================================================================
79//
80// # Design Rationale
81//
82// This normalized token usage schema unifies diverse provider formats into a
83// consistent structure based on verified specifications and code behavior.
84//
85// ## Input Token Normalization
86//
87// All three providers (Claude, Codex, Gemini) support the decomposition:
88//
89//   total_input = cached + uncached
90//
91// **Provider Mappings:**
92// - Claude:  cached = cache_read_input_tokens, uncached = input_tokens
93// - Codex:   cached = cached_input_tokens, uncached = input_tokens - cached_input_tokens
94// - Gemini:  cached = cached, uncached = input
95//
96// **Specification Guarantee:**
97// This relationship is explicitly defined in each provider's API/implementation:
98// - Claude: API documentation and usage fields
99// - Codex: codex-rs `non_cached_input()` implementation
100// - Gemini: gemini-cli telemetry calculation
101//
102// ## Output Token Normalization
103//
104// All three providers internally distinguish between token types:
105//
106//   total_output = generated + reasoning + tool
107//
108// **Provider Mappings:**
109// - Claude:  generated = output_tokens, reasoning = 0*, tool = 0*
110// - Codex:   generated = output_tokens, reasoning = reasoning_output_tokens, tool = 0
111// - Gemini:  generated = output, reasoning = thoughts, tool = tool
112//
113// *Note: Claude's content[].type allows parsing reasoning/tool separately (not yet implemented)
114//
115// **Specification Guarantee:**
116// - Codex: Explicit reasoning_output_tokens field in TokenUsage
117// - Gemini: Separate thoughts and tool fields in TokenUsage
118// - Claude: message.content[].type distinguishes "thinking" and "tool_use"
119//
120// ## What This Schema Does NOT Track
121//
122// - **Billing/Pricing**: Token costs vary by provider and usage tier
123// - **Cache Creation**: Not uniformly tracked across providers
124// - **Visibility**: Whether tokens appear in UI (e.g., hidden reasoning)
125//
126// This schema focuses solely on **observable token accounting** as reported
127// by each provider, ensuring consistent cross-provider analysis.
128
129/// Input token breakdown (cached vs uncached)
130#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
131pub struct TokenInput {
132    /// Tokens read from cache (still consume context window)
133    pub cached: u64,
134    /// Fresh tokens processed without cache
135    pub uncached: u64,
136}
137
138impl TokenInput {
139    pub fn new(cached: u64, uncached: u64) -> Self {
140        Self { cached, uncached }
141    }
142
143    pub fn total(&self) -> u64 {
144        self.cached + self.uncached
145    }
146}
147
148/// Output token breakdown (generated vs reasoning vs tool)
149#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
150pub struct TokenOutput {
151    /// Normal text generation (assistant messages)
152    pub generated: u64,
153    /// Reasoning/thinking tokens (extended thinking, o1-style)
154    pub reasoning: u64,
155    /// Tool call tokens (function calls, structured output)
156    pub tool: u64,
157}
158
159impl TokenOutput {
160    pub fn new(generated: u64, reasoning: u64, tool: u64) -> Self {
161        Self {
162            generated,
163            reasoning,
164            tool,
165        }
166    }
167
168    pub fn total(&self) -> u64 {
169        self.generated + self.reasoning + self.tool
170    }
171}
172
173/// Normalized token usage across all providers
174#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
175pub struct TokenUsagePayload {
176    pub input: TokenInput,
177    pub output: TokenOutput,
178}
179
180impl TokenUsagePayload {
181    pub fn new(input: TokenInput, output: TokenOutput) -> Self {
182        Self { input, output }
183    }
184
185    pub fn total_tokens(&self) -> u64 {
186        self.input.total() + self.output.total()
187    }
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct NotificationPayload {
192    /// Notification message text
193    pub text: String,
194    /// Optional severity level (e.g., "info", "warning", "error")
195    #[serde(default, skip_serializing_if = "Option::is_none")]
196    pub level: Option<String>,
197}