use serde::{Deserialize, Serialize, Serializer};
use super::content::{ContentPart, ToolReturnValue, UserInput};
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Event {
TurnBegin {
user_input: UserInput,
},
TurnEnd,
StepBegin {
n: u32,
},
StepInterrupted,
StepRetry {
n: u32,
next_attempt: u32,
max_attempts: u32,
wait_s: u32,
error_type: String,
status_code: Option<u32>,
},
CompactionBegin,
CompactionEnd,
StatusUpdate(StatusUpdate),
ContentPart(ContentPart),
ToolCall {
id: String,
function: ToolCallFunction,
extras: Option<serde_json::Value>,
},
ToolCallPart {
arguments_part: Option<String>,
},
ToolResult {
tool_call_id: String,
return_value: ToolReturnValue,
},
ApprovalResponse {
request_id: String,
response: ApprovalResponseKind,
feedback: Option<String>,
},
SubagentEvent {
parent_tool_call_id: Option<String>,
agent_id: Option<String>,
subagent_type: Option<String>,
event: SubagentEventPayload,
},
SteerInput {
user_input: UserInput,
},
BtwBegin {
id: String,
question: String,
},
BtwEnd {
id: String,
response: Option<String>,
error: Option<String>,
},
PlanDisplay {
content: String,
file_path: String,
},
HookTriggered {
event: String,
target: String,
hook_count: u32,
},
HookResolved {
event: String,
target: String,
action: HookAction,
reason: String,
duration_ms: u64,
},
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
pub(crate) enum FlatEvent {
TurnBegin { user_input: UserInput },
TurnEnd,
StepBegin { n: u32 },
StepInterrupted,
StepRetry {
n: u32,
next_attempt: u32,
max_attempts: u32,
wait_s: u32,
error_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
status_code: Option<u32>,
},
CompactionBegin,
CompactionEnd,
StatusUpdate(StatusUpdate),
ContentPart(ContentPart),
ToolCall {
id: String,
function: ToolCallFunction,
#[serde(skip_serializing_if = "Option::is_none")]
extras: Option<serde_json::Value>,
},
ToolCallPart {
#[serde(skip_serializing_if = "Option::is_none")]
arguments_part: Option<String>,
},
ToolResult {
tool_call_id: String,
return_value: ToolReturnValue,
},
ApprovalResponse {
request_id: String,
response: ApprovalResponseKind,
#[serde(skip_serializing_if = "Option::is_none")]
feedback: Option<String>,
},
SubagentEvent {
#[serde(skip_serializing_if = "Option::is_none")]
parent_tool_call_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
agent_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
subagent_type: Option<String>,
event: SubagentEventPayload,
},
SteerInput { user_input: UserInput },
BtwBegin { id: String, question: String },
BtwEnd {
id: String,
#[serde(skip_serializing_if = "Option::is_none")]
response: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>,
},
PlanDisplay { content: String, file_path: String },
HookTriggered { event: String, target: String, hook_count: u32 },
HookResolved { event: String, target: String, action: HookAction, reason: String, duration_ms: u64 },
}
impl From<Event> for FlatEvent {
fn from(ev: Event) -> Self {
match ev {
Event::TurnBegin { user_input } => FlatEvent::TurnBegin { user_input },
Event::TurnEnd => FlatEvent::TurnEnd,
Event::StepBegin { n } => FlatEvent::StepBegin { n },
Event::StepInterrupted => FlatEvent::StepInterrupted,
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 },
Event::CompactionBegin => FlatEvent::CompactionBegin,
Event::CompactionEnd => FlatEvent::CompactionEnd,
Event::StatusUpdate(s) => FlatEvent::StatusUpdate(s),
Event::ContentPart(c) => FlatEvent::ContentPart(c),
Event::ToolCall { id, function, extras } => FlatEvent::ToolCall { id, function, extras },
Event::ToolCallPart { arguments_part } => FlatEvent::ToolCallPart { arguments_part },
Event::ToolResult { tool_call_id, return_value } => FlatEvent::ToolResult { tool_call_id, return_value },
Event::ApprovalResponse { request_id, response, feedback } => FlatEvent::ApprovalResponse { request_id, response, feedback },
Event::SubagentEvent { parent_tool_call_id, agent_id, subagent_type, event } => FlatEvent::SubagentEvent { parent_tool_call_id, agent_id, subagent_type, event },
Event::SteerInput { user_input } => FlatEvent::SteerInput { user_input },
Event::BtwBegin { id, question } => FlatEvent::BtwBegin { id, question },
Event::BtwEnd { id, response, error } => FlatEvent::BtwEnd { id, response, error },
Event::PlanDisplay { content, file_path } => FlatEvent::PlanDisplay { content, file_path },
Event::HookTriggered { event, target, hook_count } => FlatEvent::HookTriggered { event, target, hook_count },
Event::HookResolved { event, target, action, reason, duration_ms } => FlatEvent::HookResolved { event, target, action, reason, duration_ms },
}
}
}
impl Event {
pub fn type_name(&self) -> &'static str {
match self {
Event::TurnBegin { .. } => "TurnBegin",
Event::TurnEnd => "TurnEnd",
Event::StepBegin { .. } => "StepBegin",
Event::StepInterrupted => "StepInterrupted",
Event::StepRetry { .. } => "StepRetry",
Event::CompactionBegin => "CompactionBegin",
Event::CompactionEnd => "CompactionEnd",
Event::StatusUpdate(_) => "StatusUpdate",
Event::ContentPart(_) => "ContentPart",
Event::ToolCall { .. } => "ToolCall",
Event::ToolCallPart { .. } => "ToolCallPart",
Event::ToolResult { .. } => "ToolResult",
Event::ApprovalResponse { .. } => "ApprovalResponse",
Event::SubagentEvent { .. } => "SubagentEvent",
Event::SteerInput { .. } => "SteerInput",
Event::BtwBegin { .. } => "BtwBegin",
Event::BtwEnd { .. } => "BtwEnd",
Event::PlanDisplay { .. } => "PlanDisplay",
Event::HookTriggered { .. } => "HookTriggered",
Event::HookResolved { .. } => "HookResolved",
}
}
}
impl From<FlatEvent> for Event {
fn from(ev: FlatEvent) -> Self {
match ev {
FlatEvent::TurnBegin { user_input } => Event::TurnBegin { user_input },
FlatEvent::TurnEnd => Event::TurnEnd,
FlatEvent::StepBegin { n } => Event::StepBegin { n },
FlatEvent::StepInterrupted => Event::StepInterrupted,
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 },
FlatEvent::CompactionBegin => Event::CompactionBegin,
FlatEvent::CompactionEnd => Event::CompactionEnd,
FlatEvent::StatusUpdate(s) => Event::StatusUpdate(s),
FlatEvent::ContentPart(c) => Event::ContentPart(c),
FlatEvent::ToolCall { id, function, extras } => Event::ToolCall { id, function, extras },
FlatEvent::ToolCallPart { arguments_part } => Event::ToolCallPart { arguments_part },
FlatEvent::ToolResult { tool_call_id, return_value } => Event::ToolResult { tool_call_id, return_value },
FlatEvent::ApprovalResponse { request_id, response, feedback } => Event::ApprovalResponse { request_id, response, feedback },
FlatEvent::SubagentEvent { parent_tool_call_id, agent_id, subagent_type, event } => Event::SubagentEvent { parent_tool_call_id, agent_id, subagent_type, event },
FlatEvent::SteerInput { user_input } => Event::SteerInput { user_input },
FlatEvent::BtwBegin { id, question } => Event::BtwBegin { id, question },
FlatEvent::BtwEnd { id, response, error } => Event::BtwEnd { id, response, error },
FlatEvent::PlanDisplay { content, file_path } => Event::PlanDisplay { content, file_path },
FlatEvent::HookTriggered { event, target, hook_count } => Event::HookTriggered { event, target, hook_count },
FlatEvent::HookResolved { event, target, action, reason, duration_ms } => Event::HookResolved { event, target, action, reason, duration_ms },
}
}
}
#[derive(Serialize, Deserialize)]
struct EventEnvelope {
#[serde(rename = "type")]
type_name: String,
payload: serde_json::Value,
}
impl Serialize for Event {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Event::ContentPart(part) => {
let payload = serde_json::to_value(part).map_err(serde::ser::Error::custom)?;
EventEnvelope {
type_name: "ContentPart".to_string(),
payload,
}
.serialize(serializer)
}
Event::ToolCall { id, function, extras } => {
#[derive(Serialize)]
struct ToolCallPayload<'a> {
#[serde(rename = "type")]
type_name: &'a str,
id: &'a str,
function: &'a ToolCallFunction,
#[serde(skip_serializing_if = "Option::is_none")]
extras: &'a Option<serde_json::Value>,
}
let payload = serde_json::to_value(&ToolCallPayload {
type_name: "function",
id,
function,
extras,
})
.map_err(serde::ser::Error::custom)?;
EventEnvelope {
type_name: "ToolCall".to_string(),
payload,
}
.serialize(serializer)
}
_ => {
let flat = FlatEvent::from(self.clone());
let mut value = serde_json::to_value(&flat).map_err(serde::ser::Error::custom)?;
let obj = value
.as_object_mut()
.ok_or_else(|| serde::ser::Error::custom("expected object"))?;
let type_name = obj
.remove("type")
.and_then(|v| v.as_str().map(String::from))
.ok_or_else(|| serde::ser::Error::custom("missing type"))?;
EventEnvelope { type_name, payload: value }.serialize(serializer)
}
}
}
}
impl<'de> Deserialize<'de> for Event {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let envelope = EventEnvelope::deserialize(deserializer)?;
match envelope.type_name.as_str() {
"ContentPart" => {
let part: ContentPart =
serde_json::from_value(envelope.payload).map_err(serde::de::Error::custom)?;
Ok(Event::ContentPart(part))
}
_ => {
let mut value = envelope.payload;
if let Some(obj) = value.as_object_mut() {
obj.insert(
"type".to_string(),
serde_json::Value::String(envelope.type_name),
);
}
let flat: FlatEvent =
serde_json::from_value(value).map_err(serde::de::Error::custom)?;
Ok(Event::from(flat))
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SubagentEventPayload {
#[serde(rename = "type")]
pub type_name: String,
pub payload: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StatusUpdate {
#[serde(skip_serializing_if = "Option::is_none")]
pub context_usage: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context_tokens: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_context_tokens: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token_usage: Option<TokenUsage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub plan_mode: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TokenUsage {
pub input_other: u64,
pub output: u64,
pub input_cache_read: u64,
pub input_cache_creation: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ToolCallFunction {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ApprovalResponseKind {
Approve,
#[serde(rename = "approve_for_session")]
ApproveForSession,
Reject,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum HookAction {
Allow,
Block,
}