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