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
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct MessagePayload {
67 /// Response text
68 pub text: String,
69}
70
71// ============================================================================
72// Token Usage Normalization
73// ============================================================================
74//
75// # Design Rationale
76//
77// This normalized token usage schema unifies diverse provider formats into a
78// consistent structure based on verified specifications and code behavior.
79//
80// ## Input Token Normalization
81//
82// All three providers (Claude, Codex, Gemini) support the decomposition:
83//
84// total_input = cached + uncached
85//
86// **Provider Mappings:**
87// - Claude: cached = cache_read_input_tokens, uncached = input_tokens
88// - Codex: cached = cached_input_tokens, uncached = input_tokens - cached_input_tokens
89// - Gemini: cached = cached, uncached = input
90//
91// **Specification Guarantee:**
92// This relationship is explicitly defined in each provider's API/implementation:
93// - Claude: API documentation and usage fields
94// - Codex: codex-rs `non_cached_input()` implementation
95// - Gemini: gemini-cli telemetry calculation
96//
97// ## Output Token Normalization
98//
99// All three providers internally distinguish between token types:
100//
101// total_output = generated + reasoning + tool
102//
103// **Provider Mappings:**
104// - Claude: generated = output_tokens, reasoning = 0*, tool = 0*
105// - Codex: generated = output_tokens, reasoning = reasoning_output_tokens, tool = 0
106// - Gemini: generated = output, reasoning = thoughts, tool = tool
107//
108// *Note: Claude's content[].type allows parsing reasoning/tool separately (not yet implemented)
109//
110// **Specification Guarantee:**
111// - Codex: Explicit reasoning_output_tokens field in TokenUsage
112// - Gemini: Separate thoughts and tool fields in TokenUsage
113// - Claude: message.content[].type distinguishes "thinking" and "tool_use"
114//
115// ## What This Schema Does NOT Track
116//
117// - **Billing/Pricing**: Token costs vary by provider and usage tier
118// - **Cache Creation**: Not uniformly tracked across providers
119// - **Visibility**: Whether tokens appear in UI (e.g., hidden reasoning)
120//
121// This schema focuses solely on **observable token accounting** as reported
122// by each provider, ensuring consistent cross-provider analysis.
123
124/// Input token breakdown (cached vs uncached)
125#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
126pub struct TokenInput {
127 /// Tokens read from cache (still consume context window)
128 pub cached: u64,
129 /// Fresh tokens processed without cache
130 pub uncached: u64,
131}
132
133impl TokenInput {
134 pub fn new(cached: u64, uncached: u64) -> Self {
135 Self { cached, uncached }
136 }
137
138 pub fn total(&self) -> u64 {
139 self.cached + self.uncached
140 }
141}
142
143/// Output token breakdown (generated vs reasoning vs tool)
144#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
145pub struct TokenOutput {
146 /// Normal text generation (assistant messages)
147 pub generated: u64,
148 /// Reasoning/thinking tokens (extended thinking, o1-style)
149 pub reasoning: u64,
150 /// Tool call tokens (function calls, structured output)
151 pub tool: u64,
152}
153
154impl TokenOutput {
155 pub fn new(generated: u64, reasoning: u64, tool: u64) -> Self {
156 Self {
157 generated,
158 reasoning,
159 tool,
160 }
161 }
162
163 pub fn total(&self) -> u64 {
164 self.generated + self.reasoning + self.tool
165 }
166}
167
168/// Normalized token usage across all providers
169#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
170pub struct TokenUsagePayload {
171 pub input: TokenInput,
172 pub output: TokenOutput,
173}
174
175impl TokenUsagePayload {
176 pub fn new(input: TokenInput, output: TokenOutput) -> Self {
177 Self { input, output }
178 }
179
180 pub fn total_tokens(&self) -> u64 {
181 self.input.total() + self.output.total()
182 }
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct NotificationPayload {
187 /// Notification message text
188 pub text: String,
189 /// Optional severity level (e.g., "info", "warning", "error")
190 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub level: Option<String>,
192}