Skip to main content

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}