1use serde::{Deserialize, Serialize, Serializer};
2
3use super::content::{ContentPart, ToolReturnValue, UserInput};
4
5#[derive(Debug, Clone, PartialEq)]
13#[non_exhaustive]
14pub enum Event {
15 TurnBegin {
17 user_input: UserInput,
19 },
20 TurnEnd,
22 StepBegin {
24 n: u32,
26 },
27 StepInterrupted,
29 StepRetry {
33 n: u32,
35 next_attempt: u32,
37 max_attempts: u32,
39 wait_s: u32,
41 error_type: String,
43 status_code: Option<u32>,
45 },
46 CompactionBegin,
48 CompactionEnd,
50 StatusUpdate(StatusUpdate),
52 ContentPart(ContentPart),
54 ToolCall {
59 id: String,
61 function: ToolCallFunction,
63 extras: Option<serde_json::Value>,
65 },
66 ToolCallPart {
68 arguments_part: Option<String>,
70 },
71 ToolResult {
73 tool_call_id: String,
75 return_value: ToolReturnValue,
77 },
78 ApprovalResponse {
80 request_id: String,
82 response: ApprovalResponseKind,
84 feedback: Option<String>,
86 },
87 SubagentEvent {
89 parent_tool_call_id: Option<String>,
91 agent_id: Option<String>,
93 subagent_type: Option<String>,
95 event: SubagentEventPayload,
97 },
98 SteerInput {
100 user_input: UserInput,
102 },
103 BtwBegin {
107 id: String,
109 question: String,
111 },
112 BtwEnd {
116 id: String,
118 response: Option<String>,
120 error: Option<String>,
122 },
123 PlanDisplay {
125 content: String,
127 file_path: String,
129 },
130 HookTriggered {
132 event: String,
134 target: String,
136 hook_count: u32,
138 },
139 HookResolved {
141 event: String,
143 target: String,
145 action: HookAction,
147 reason: String,
149 duration_ms: u64,
151 },
152}
153
154#[derive(Serialize, Deserialize)]
159#[serde(tag = "type")]
160pub(crate) enum FlatEvent {
161 TurnBegin { user_input: UserInput },
162 TurnEnd,
163 StepBegin { n: u32 },
164 StepInterrupted,
165 StepRetry {
166 n: u32,
167 next_attempt: u32,
168 max_attempts: u32,
169 wait_s: u32,
170 error_type: String,
171 #[serde(skip_serializing_if = "Option::is_none")]
172 status_code: Option<u32>,
173 },
174 CompactionBegin,
175 CompactionEnd,
176 StatusUpdate(StatusUpdate),
177 ContentPart(ContentPart),
178 ToolCall {
179 id: String,
180 function: ToolCallFunction,
181 #[serde(skip_serializing_if = "Option::is_none")]
182 extras: Option<serde_json::Value>,
183 },
184 ToolCallPart {
185 #[serde(skip_serializing_if = "Option::is_none")]
186 arguments_part: Option<String>,
187 },
188 ToolResult {
189 tool_call_id: String,
190 return_value: ToolReturnValue,
191 },
192 ApprovalResponse {
193 request_id: String,
194 response: ApprovalResponseKind,
195 #[serde(skip_serializing_if = "Option::is_none")]
196 feedback: Option<String>,
197 },
198 SubagentEvent {
199 #[serde(skip_serializing_if = "Option::is_none")]
200 parent_tool_call_id: Option<String>,
201 #[serde(skip_serializing_if = "Option::is_none")]
202 agent_id: Option<String>,
203 #[serde(skip_serializing_if = "Option::is_none")]
204 subagent_type: Option<String>,
205 event: SubagentEventPayload,
206 },
207 SteerInput { user_input: UserInput },
208 BtwBegin { id: String, question: String },
209 BtwEnd {
210 id: String,
211 #[serde(skip_serializing_if = "Option::is_none")]
212 response: Option<String>,
213 #[serde(skip_serializing_if = "Option::is_none")]
214 error: Option<String>,
215 },
216 PlanDisplay { content: String, file_path: String },
217 HookTriggered { event: String, target: String, hook_count: u32 },
218 HookResolved { event: String, target: String, action: HookAction, reason: String, duration_ms: u64 },
219}
220
221impl From<Event> for FlatEvent {
222 fn from(ev: Event) -> Self {
223 match ev {
224 Event::TurnBegin { user_input } => FlatEvent::TurnBegin { user_input },
225 Event::TurnEnd => FlatEvent::TurnEnd,
226 Event::StepBegin { n } => FlatEvent::StepBegin { n },
227 Event::StepInterrupted => FlatEvent::StepInterrupted,
228 Event::StepRetry { n, next_attempt, max_attempts, wait_s, error_type, status_code } => FlatEvent::StepRetry { n, next_attempt, max_attempts, wait_s, error_type, status_code },
229 Event::CompactionBegin => FlatEvent::CompactionBegin,
230 Event::CompactionEnd => FlatEvent::CompactionEnd,
231 Event::StatusUpdate(s) => FlatEvent::StatusUpdate(s),
232 Event::ContentPart(c) => FlatEvent::ContentPart(c),
233 Event::ToolCall { id, function, extras } => FlatEvent::ToolCall { id, function, extras },
234 Event::ToolCallPart { arguments_part } => FlatEvent::ToolCallPart { arguments_part },
235 Event::ToolResult { tool_call_id, return_value } => FlatEvent::ToolResult { tool_call_id, return_value },
236 Event::ApprovalResponse { request_id, response, feedback } => FlatEvent::ApprovalResponse { request_id, response, feedback },
237 Event::SubagentEvent { parent_tool_call_id, agent_id, subagent_type, event } => FlatEvent::SubagentEvent { parent_tool_call_id, agent_id, subagent_type, event },
238 Event::SteerInput { user_input } => FlatEvent::SteerInput { user_input },
239 Event::BtwBegin { id, question } => FlatEvent::BtwBegin { id, question },
240 Event::BtwEnd { id, response, error } => FlatEvent::BtwEnd { id, response, error },
241 Event::PlanDisplay { content, file_path } => FlatEvent::PlanDisplay { content, file_path },
242 Event::HookTriggered { event, target, hook_count } => FlatEvent::HookTriggered { event, target, hook_count },
243 Event::HookResolved { event, target, action, reason, duration_ms } => FlatEvent::HookResolved { event, target, action, reason, duration_ms },
244 }
245 }
246}
247
248impl Event {
249 pub fn type_name(&self) -> &'static str {
253 match self {
254 Event::TurnBegin { .. } => "TurnBegin",
255 Event::TurnEnd => "TurnEnd",
256 Event::StepBegin { .. } => "StepBegin",
257 Event::StepInterrupted => "StepInterrupted",
258 Event::StepRetry { .. } => "StepRetry",
259 Event::CompactionBegin => "CompactionBegin",
260 Event::CompactionEnd => "CompactionEnd",
261 Event::StatusUpdate(_) => "StatusUpdate",
262 Event::ContentPart(_) => "ContentPart",
263 Event::ToolCall { .. } => "ToolCall",
264 Event::ToolCallPart { .. } => "ToolCallPart",
265 Event::ToolResult { .. } => "ToolResult",
266 Event::ApprovalResponse { .. } => "ApprovalResponse",
267 Event::SubagentEvent { .. } => "SubagentEvent",
268 Event::SteerInput { .. } => "SteerInput",
269 Event::BtwBegin { .. } => "BtwBegin",
270 Event::BtwEnd { .. } => "BtwEnd",
271 Event::PlanDisplay { .. } => "PlanDisplay",
272 Event::HookTriggered { .. } => "HookTriggered",
273 Event::HookResolved { .. } => "HookResolved",
274 }
275 }
276}
277
278impl From<FlatEvent> for Event {
279 fn from(ev: FlatEvent) -> Self {
280 match ev {
281 FlatEvent::TurnBegin { user_input } => Event::TurnBegin { user_input },
282 FlatEvent::TurnEnd => Event::TurnEnd,
283 FlatEvent::StepBegin { n } => Event::StepBegin { n },
284 FlatEvent::StepInterrupted => Event::StepInterrupted,
285 FlatEvent::StepRetry { n, next_attempt, max_attempts, wait_s, error_type, status_code } => Event::StepRetry { n, next_attempt, max_attempts, wait_s, error_type, status_code },
286 FlatEvent::CompactionBegin => Event::CompactionBegin,
287 FlatEvent::CompactionEnd => Event::CompactionEnd,
288 FlatEvent::StatusUpdate(s) => Event::StatusUpdate(s),
289 FlatEvent::ContentPart(c) => Event::ContentPart(c),
290 FlatEvent::ToolCall { id, function, extras } => Event::ToolCall { id, function, extras },
291 FlatEvent::ToolCallPart { arguments_part } => Event::ToolCallPart { arguments_part },
292 FlatEvent::ToolResult { tool_call_id, return_value } => Event::ToolResult { tool_call_id, return_value },
293 FlatEvent::ApprovalResponse { request_id, response, feedback } => Event::ApprovalResponse { request_id, response, feedback },
294 FlatEvent::SubagentEvent { parent_tool_call_id, agent_id, subagent_type, event } => Event::SubagentEvent { parent_tool_call_id, agent_id, subagent_type, event },
295 FlatEvent::SteerInput { user_input } => Event::SteerInput { user_input },
296 FlatEvent::BtwBegin { id, question } => Event::BtwBegin { id, question },
297 FlatEvent::BtwEnd { id, response, error } => Event::BtwEnd { id, response, error },
298 FlatEvent::PlanDisplay { content, file_path } => Event::PlanDisplay { content, file_path },
299 FlatEvent::HookTriggered { event, target, hook_count } => Event::HookTriggered { event, target, hook_count },
300 FlatEvent::HookResolved { event, target, action, reason, duration_ms } => Event::HookResolved { event, target, action, reason, duration_ms },
301 }
302 }
303}
304
305#[derive(Serialize, Deserialize)]
310struct EventEnvelope {
311 #[serde(rename = "type")]
312 type_name: String,
313 payload: serde_json::Value,
314}
315
316impl Serialize for Event {
317 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
318 match self {
319 Event::ContentPart(part) => {
322 let payload = serde_json::to_value(part).map_err(serde::ser::Error::custom)?;
323 EventEnvelope {
324 type_name: "ContentPart".to_string(),
325 payload,
326 }
327 .serialize(serializer)
328 }
329 Event::ToolCall { id, function, extras } => {
332 #[derive(Serialize)]
333 struct ToolCallPayload<'a> {
334 #[serde(rename = "type")]
335 type_name: &'a str,
336 id: &'a str,
337 function: &'a ToolCallFunction,
338 #[serde(skip_serializing_if = "Option::is_none")]
339 extras: &'a Option<serde_json::Value>,
340 }
341 let payload = serde_json::to_value(&ToolCallPayload {
342 type_name: "function",
343 id,
344 function,
345 extras,
346 })
347 .map_err(serde::ser::Error::custom)?;
348 EventEnvelope {
349 type_name: "ToolCall".to_string(),
350 payload,
351 }
352 .serialize(serializer)
353 }
354 _ => {
355 let flat = FlatEvent::from(self.clone());
356 let mut value = serde_json::to_value(&flat).map_err(serde::ser::Error::custom)?;
357 let obj = value
358 .as_object_mut()
359 .ok_or_else(|| serde::ser::Error::custom("expected object"))?;
360 let type_name = obj
361 .remove("type")
362 .and_then(|v| v.as_str().map(String::from))
363 .ok_or_else(|| serde::ser::Error::custom("missing type"))?;
364 EventEnvelope { type_name, payload: value }.serialize(serializer)
365 }
366 }
367 }
368}
369
370impl<'de> Deserialize<'de> for Event {
371 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
372 let envelope = EventEnvelope::deserialize(deserializer)?;
373 match envelope.type_name.as_str() {
374 "ContentPart" => {
375 let part: ContentPart =
376 serde_json::from_value(envelope.payload).map_err(serde::de::Error::custom)?;
377 Ok(Event::ContentPart(part))
378 }
379 _ => {
380 let mut value = envelope.payload;
381 if let Some(obj) = value.as_object_mut() {
382 obj.insert(
383 "type".to_string(),
384 serde_json::Value::String(envelope.type_name),
385 );
386 }
387 let flat: FlatEvent =
388 serde_json::from_value(value).map_err(serde::de::Error::custom)?;
389 Ok(Event::from(flat))
390 }
391 }
392 }
393}
394
395#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
400pub struct SubagentEventPayload {
401 #[serde(rename = "type")]
403 pub type_name: String,
404 pub payload: serde_json::Value,
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
410pub struct StatusUpdate {
411 #[serde(skip_serializing_if = "Option::is_none")]
413 pub context_usage: Option<f64>,
414 #[serde(skip_serializing_if = "Option::is_none")]
416 pub context_tokens: Option<u64>,
417 #[serde(skip_serializing_if = "Option::is_none")]
419 pub max_context_tokens: Option<u64>,
420 #[serde(skip_serializing_if = "Option::is_none")]
422 pub token_usage: Option<TokenUsage>,
423 #[serde(skip_serializing_if = "Option::is_none")]
425 pub message_id: Option<String>,
426 #[serde(skip_serializing_if = "Option::is_none")]
428 pub plan_mode: Option<bool>,
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
433pub struct TokenUsage {
434 pub input_other: u64,
436 pub output: u64,
438 pub input_cache_read: u64,
440 pub input_cache_creation: u64,
442}
443
444#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
446pub struct ToolCallFunction {
447 pub name: String,
449 #[serde(skip_serializing_if = "Option::is_none")]
451 pub arguments: Option<String>,
452}
453
454#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
456#[serde(rename_all = "snake_case")]
457#[non_exhaustive]
458pub enum ApprovalResponseKind {
459 Approve,
461 #[serde(rename = "approve_for_session")]
463 ApproveForSession,
464 Reject,
466}
467
468#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
470#[serde(rename_all = "snake_case")]
471#[non_exhaustive]
472pub enum HookAction {
473 Allow,
475 Block,
477}