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 /// 8. Slash command invocation (e.g., /commit, /review-pr)
39 SlashCommand(SlashCommandPayload),
40
41 /// 9. Background task queue operation
42 QueueOperation(QueueOperationPayload),
43
44 /// 10. Session summary
45 Summary(SummaryPayload),
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct UserPayload {
50 /// User input text
51 pub text: String,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct ReasoningPayload {
56 /// Reasoning/thinking content
57 pub text: String,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct ToolResultPayload {
62 /// Tool execution result (text, JSON string, error message, etc.)
63 pub output: String,
64
65 /// Logical parent (Tool Call) reference ID
66 /// Separate from parent_id (time-series parent) to explicitly identify which call this result belongs to
67 pub tool_call_id: Uuid,
68
69 /// Execution success or failure
70 #[serde(default)]
71 pub is_error: bool,
72
73 /// Agent ID if this result spawned a subagent (e.g., "be466c0a")
74 /// Used to link sidechain sessions back to their parent turn/step
75 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub agent_id: Option<String>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct MessagePayload {
81 /// Response text
82 pub text: String,
83}
84
85// ============================================================================
86// Token Usage Normalization
87// ============================================================================
88//
89// # Design Rationale
90//
91// This normalized token usage schema unifies diverse provider formats into a
92// consistent structure based on verified specifications and code behavior.
93//
94// ## Input Token Normalization
95//
96// All three providers (Claude, Codex, Gemini) support the decomposition:
97//
98// total_input = cached + uncached
99//
100// **Provider Mappings:**
101// - Claude: cached = cache_read_input_tokens, uncached = input_tokens
102// - Codex: cached = cached_input_tokens, uncached = input_tokens - cached_input_tokens
103// - Gemini: cached = cached, uncached = input
104//
105// **Specification Guarantee:**
106// This relationship is explicitly defined in each provider's API/implementation:
107// - Claude: API documentation and usage fields
108// - Codex: codex-rs `non_cached_input()` implementation
109// - Gemini: gemini-cli telemetry calculation
110//
111// ## Output Token Normalization
112//
113// All three providers internally distinguish between token types:
114//
115// total_output = generated + reasoning + tool
116//
117// **Provider Mappings:**
118// - Claude: generated = output_tokens, reasoning = 0*, tool = 0*
119// - Codex: generated = output_tokens, reasoning = reasoning_output_tokens, tool = 0
120// - Gemini: generated = output, reasoning = thoughts, tool = tool
121//
122// *Note: Claude's content[].type allows parsing reasoning/tool separately (not yet implemented)
123//
124// **Specification Guarantee:**
125// - Codex: Explicit reasoning_output_tokens field in TokenUsage
126// - Gemini: Separate thoughts and tool fields in TokenUsage
127// - Claude: message.content[].type distinguishes "thinking" and "tool_use"
128//
129// ## What This Schema Does NOT Track
130//
131// - **Billing/Pricing**: Token costs vary by provider and usage tier
132// - **Cache Creation**: Not uniformly tracked across providers
133// - **Visibility**: Whether tokens appear in UI (e.g., hidden reasoning)
134//
135// This schema focuses solely on **observable token accounting** as reported
136// by each provider, ensuring consistent cross-provider analysis.
137
138/// Input token breakdown (cached vs uncached)
139#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
140pub struct TokenInput {
141 /// Tokens read from cache (still consume context window)
142 pub cached: u64,
143 /// Fresh tokens processed without cache
144 pub uncached: u64,
145}
146
147impl TokenInput {
148 pub fn new(cached: u64, uncached: u64) -> Self {
149 Self { cached, uncached }
150 }
151
152 pub fn total(&self) -> u64 {
153 self.cached + self.uncached
154 }
155}
156
157/// Output token breakdown (generated vs reasoning vs tool)
158#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
159pub struct TokenOutput {
160 /// Normal text generation (assistant messages)
161 pub generated: u64,
162 /// Reasoning/thinking tokens (extended thinking, o1-style)
163 pub reasoning: u64,
164 /// Tool call tokens (function calls, structured output)
165 pub tool: u64,
166}
167
168impl TokenOutput {
169 pub fn new(generated: u64, reasoning: u64, tool: u64) -> Self {
170 Self {
171 generated,
172 reasoning,
173 tool,
174 }
175 }
176
177 pub fn total(&self) -> u64 {
178 self.generated + self.reasoning + self.tool
179 }
180}
181
182/// Normalized token usage across all providers
183#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
184pub struct TokenUsagePayload {
185 pub input: TokenInput,
186 pub output: TokenOutput,
187}
188
189impl TokenUsagePayload {
190 pub fn new(input: TokenInput, output: TokenOutput) -> Self {
191 Self { input, output }
192 }
193
194 pub fn total_tokens(&self) -> u64 {
195 self.input.total() + self.output.total()
196 }
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct NotificationPayload {
201 /// Notification message text
202 pub text: String,
203 /// Optional severity level (e.g., "info", "warning", "error")
204 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub level: Option<String>,
206}
207
208/// Slash command invocation (e.g., /commit, /review-pr, /skaffold-repo)
209#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct SlashCommandPayload {
211 /// Command name with leading slash (e.g., "/commit", "/skaffold-repo")
212 pub name: String,
213 /// Optional command arguments
214 #[serde(default, skip_serializing_if = "Option::is_none")]
215 pub args: Option<String>,
216}
217
218/// Background task queue operation (enqueue/dequeue)
219#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct QueueOperationPayload {
221 /// Operation type (e.g., "enqueue", "dequeue")
222 pub operation: String,
223 /// Task content description
224 #[serde(default, skip_serializing_if = "Option::is_none")]
225 pub content: Option<String>,
226 /// Task identifier
227 #[serde(default, skip_serializing_if = "Option::is_none")]
228 pub task_id: Option<String>,
229}
230
231/// Session summary record
232#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct SummaryPayload {
234 /// Summary text
235 pub summary: String,
236 /// Leaf UUID reference
237 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub leaf_uuid: Option<String>,
239}