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 } => Self::TurnBegin { user_input },
Event::TurnEnd => Self::TurnEnd,
Event::StepBegin { n } => Self::StepBegin { n },
Event::StepInterrupted => Self::StepInterrupted,
Event::StepRetry {
n,
next_attempt,
max_attempts,
wait_s,
error_type,
status_code,
} => Self::StepRetry {
n,
next_attempt,
max_attempts,
wait_s,
error_type,
status_code,
},
Event::CompactionBegin => Self::CompactionBegin,
Event::CompactionEnd => Self::CompactionEnd,
Event::StatusUpdate(s) => Self::StatusUpdate(s),
Event::ContentPart(c) => Self::ContentPart(c),
Event::ToolCall {
id,
function,
extras,
} => Self::ToolCall {
id,
function,
extras,
},
Event::ToolCallPart { arguments_part } => Self::ToolCallPart { arguments_part },
Event::ToolResult {
tool_call_id,
return_value,
} => Self::ToolResult {
tool_call_id,
return_value,
},
Event::ApprovalResponse {
request_id,
response,
feedback,
} => Self::ApprovalResponse {
request_id,
response,
feedback,
},
Event::SubagentEvent {
parent_tool_call_id,
agent_id,
subagent_type,
event,
} => Self::SubagentEvent {
parent_tool_call_id,
agent_id,
subagent_type,
event,
},
Event::SteerInput { user_input } => Self::SteerInput { user_input },
Event::BtwBegin { id, question } => Self::BtwBegin { id, question },
Event::BtwEnd {
id,
response,
error,
} => Self::BtwEnd {
id,
response,
error,
},
Event::PlanDisplay { content, file_path } => Self::PlanDisplay { content, file_path },
Event::HookTriggered {
event,
target,
hook_count,
} => Self::HookTriggered {
event,
target,
hook_count,
},
Event::HookResolved {
event,
target,
action,
reason,
duration_ms,
} => Self::HookResolved {
event,
target,
action,
reason,
duration_ms,
},
}
}
}
impl Event {
#[must_use]
pub const fn type_name(&self) -> &'static str {
match self {
Self::TurnBegin { .. } => "TurnBegin",
Self::TurnEnd => "TurnEnd",
Self::StepBegin { .. } => "StepBegin",
Self::StepInterrupted => "StepInterrupted",
Self::StepRetry { .. } => "StepRetry",
Self::CompactionBegin => "CompactionBegin",
Self::CompactionEnd => "CompactionEnd",
Self::StatusUpdate(_) => "StatusUpdate",
Self::ContentPart(_) => "ContentPart",
Self::ToolCall { .. } => "ToolCall",
Self::ToolCallPart { .. } => "ToolCallPart",
Self::ToolResult { .. } => "ToolResult",
Self::ApprovalResponse { .. } => "ApprovalResponse",
Self::SubagentEvent { .. } => "SubagentEvent",
Self::SteerInput { .. } => "SteerInput",
Self::BtwBegin { .. } => "BtwBegin",
Self::BtwEnd { .. } => "BtwEnd",
Self::PlanDisplay { .. } => "PlanDisplay",
Self::HookTriggered { .. } => "HookTriggered",
Self::HookResolved { .. } => "HookResolved",
}
}
}
impl From<FlatEvent> for Event {
fn from(ev: FlatEvent) -> Self {
match ev {
FlatEvent::TurnBegin { user_input } => Self::TurnBegin { user_input },
FlatEvent::TurnEnd => Self::TurnEnd,
FlatEvent::StepBegin { n } => Self::StepBegin { n },
FlatEvent::StepInterrupted => Self::StepInterrupted,
FlatEvent::StepRetry {
n,
next_attempt,
max_attempts,
wait_s,
error_type,
status_code,
} => Self::StepRetry {
n,
next_attempt,
max_attempts,
wait_s,
error_type,
status_code,
},
FlatEvent::CompactionBegin => Self::CompactionBegin,
FlatEvent::CompactionEnd => Self::CompactionEnd,
FlatEvent::StatusUpdate(s) => Self::StatusUpdate(s),
FlatEvent::ContentPart(c) => Self::ContentPart(c),
FlatEvent::ToolCall {
id,
function,
extras,
} => Self::ToolCall {
id,
function,
extras,
},
FlatEvent::ToolCallPart { arguments_part } => Self::ToolCallPart { arguments_part },
FlatEvent::ToolResult {
tool_call_id,
return_value,
} => Self::ToolResult {
tool_call_id,
return_value,
},
FlatEvent::ApprovalResponse {
request_id,
response,
feedback,
} => Self::ApprovalResponse {
request_id,
response,
feedback,
},
FlatEvent::SubagentEvent {
parent_tool_call_id,
agent_id,
subagent_type,
event,
} => Self::SubagentEvent {
parent_tool_call_id,
agent_id,
subagent_type,
event,
},
FlatEvent::SteerInput { user_input } => Self::SteerInput { user_input },
FlatEvent::BtwBegin { id, question } => Self::BtwBegin { id, question },
FlatEvent::BtwEnd {
id,
response,
error,
} => Self::BtwEnd {
id,
response,
error,
},
FlatEvent::PlanDisplay { content, file_path } => {
Self::PlanDisplay { content, file_path }
}
FlatEvent::HookTriggered {
event,
target,
hook_count,
} => Self::HookTriggered {
event,
target,
hook_count,
},
FlatEvent::HookResolved {
event,
target,
action,
reason,
duration_ms,
} => Self::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 {
Self::ContentPart(part) => {
let payload = serde_json::to_value(part).map_err(serde::ser::Error::custom)?;
EventEnvelope {
type_name: "ContentPart".to_string(),
payload,
}
.serialize(serializer)
}
Self::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)?;
if envelope.type_name.as_str() == "ContentPart" {
let part: ContentPart =
serde_json::from_value(envelope.payload).map_err(serde::de::Error::custom)?;
Ok(Self::ContentPart(part))
} else {
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(Self::from(flat))
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
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, Eq)]
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, Eq)]
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,
}