use serde::{Deserialize, Serialize};
use super::content::ContentBlock;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum UserEvent {
#[serde(rename = "user.message")]
Message {
content: Vec<ContentBlock>,
},
#[serde(rename = "user.interrupt")]
Interrupt {},
#[serde(rename = "user.tool_confirmation")]
ToolConfirmation {
tool_use_id: String,
result: ConfirmationResult,
#[serde(skip_serializing_if = "Option::is_none")]
deny_message: Option<String>,
},
#[serde(rename = "user.custom_tool_result")]
CustomToolResult {
custom_tool_use_id: String,
content: Vec<ContentBlock>,
},
#[serde(rename = "user.tool_result")]
ToolResult {
tool_use_id: String,
content: Vec<ContentBlock>,
},
#[serde(rename = "user.define_outcome")]
DefineOutcome {
criteria: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ConfirmationResult {
Allow,
Deny,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum SessionEvent {
#[serde(rename = "agent.message")]
Message {
content: Vec<ContentBlock>,
seq: u64,
},
#[serde(rename = "agent.tool_use")]
ToolUse {
tool_use_id: String,
name: String,
input: serde_json::Value,
seq: u64,
},
#[serde(rename = "agent.custom_tool_use")]
CustomToolUse {
custom_tool_use_id: String,
name: String,
input: serde_json::Value,
seq: u64,
},
#[serde(rename = "agent.mcp_tool_use")]
McpToolUse {
tool_use_id: String,
name: String,
input: serde_json::Value,
seq: u64,
},
#[serde(rename = "status.running")]
StatusRunning {
seq: u64,
},
#[serde(rename = "status.idle")]
StatusIdle {
seq: u64,
stop_reason: Option<StopReason>,
#[serde(skip_serializing_if = "Option::is_none")]
usage: Option<crate::usage::UsageReport>,
},
#[serde(rename = "error")]
Error {
code: String,
message: String,
seq: u64,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "reason", rename_all = "snake_case")]
#[non_exhaustive]
pub enum StopReason {
EndTurn,
RequiresAction {
event_ids: Vec<String>,
},
MaxTokens,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_user_message_serialization() {
let event =
UserEvent::Message { content: vec![ContentBlock::Text { text: "Hello".to_string() }] };
let value = serde_json::to_value(&event).unwrap();
assert_eq!(value["type"], "user.message");
assert_eq!(value["content"][0]["type"], "text");
assert_eq!(value["content"][0]["text"], "Hello");
}
#[test]
fn test_user_interrupt_serialization() {
let event = UserEvent::Interrupt {};
let value = serde_json::to_value(&event).unwrap();
assert_eq!(value["type"], "user.interrupt");
}
#[test]
fn test_user_tool_confirmation_serialization() {
let event = UserEvent::ToolConfirmation {
tool_use_id: "tu_123".to_string(),
result: ConfirmationResult::Allow,
deny_message: None,
};
let value = serde_json::to_value(&event).unwrap();
assert_eq!(value["type"], "user.tool_confirmation");
assert_eq!(value["tool_use_id"], "tu_123");
assert_eq!(value["result"], "allow");
assert!(value.get("deny_message").is_none());
}
#[test]
fn test_user_tool_confirmation_with_deny() {
let event = UserEvent::ToolConfirmation {
tool_use_id: "tu_456".to_string(),
result: ConfirmationResult::Deny,
deny_message: Some("Not authorized".to_string()),
};
let value = serde_json::to_value(&event).unwrap();
assert_eq!(value["type"], "user.tool_confirmation");
assert_eq!(value["result"], "deny");
assert_eq!(value["deny_message"], "Not authorized");
}
#[test]
fn test_user_custom_tool_result_serialization() {
let event = UserEvent::CustomToolResult {
custom_tool_use_id: "ctu_789".to_string(),
content: vec![ContentBlock::Text { text: "result data".to_string() }],
};
let value = serde_json::to_value(&event).unwrap();
assert_eq!(value["type"], "user.custom_tool_result");
assert_eq!(value["custom_tool_use_id"], "ctu_789");
}
#[test]
fn test_user_tool_result_serialization() {
let event = UserEvent::ToolResult {
tool_use_id: "tu_self_001".to_string(),
content: vec![ContentBlock::Text { text: "tool output".to_string() }],
};
let value = serde_json::to_value(&event).unwrap();
assert_eq!(value["type"], "user.tool_result");
assert_eq!(value["tool_use_id"], "tu_self_001");
}
#[test]
fn test_user_define_outcome_serialization() {
let event =
UserEvent::DefineOutcome { criteria: "Task is completed successfully".to_string() };
let value = serde_json::to_value(&event).unwrap();
assert_eq!(value["type"], "user.define_outcome");
assert_eq!(value["criteria"], "Task is completed successfully");
}
#[test]
fn test_user_event_unknown_type_rejected() {
let json_str = r#"{"type": "user.unknown", "data": "something"}"#;
let result: Result<UserEvent, _> = serde_json::from_str(json_str);
assert!(result.is_err(), "Unknown type should be rejected");
}
#[test]
fn test_user_event_round_trip() {
let event = UserEvent::Message {
content: vec![ContentBlock::Text { text: "Round trip".to_string() }],
};
let json = serde_json::to_string(&event).unwrap();
let deserialized: UserEvent = serde_json::from_str(&json).unwrap();
match deserialized {
UserEvent::Message { content } => {
assert_eq!(content.len(), 1);
match &content[0] {
ContentBlock::Text { text } => assert_eq!(text, "Round trip"),
_ => panic!("Expected Text content block"),
}
}
_ => panic!("Expected Message variant"),
}
}
#[test]
fn test_session_message_serialization() {
let event = SessionEvent::Message {
content: vec![ContentBlock::Text { text: "Hi there".to_string() }],
seq: 1,
};
let value = serde_json::to_value(&event).unwrap();
assert_eq!(value["type"], "agent.message");
assert_eq!(value["seq"], 1);
assert_eq!(value["content"][0]["text"], "Hi there");
}
#[test]
fn test_session_tool_use_serialization() {
let event = SessionEvent::ToolUse {
tool_use_id: "tu_001".to_string(),
name: "search".to_string(),
input: json!({"query": "rust async"}),
seq: 2,
};
let value = serde_json::to_value(&event).unwrap();
assert_eq!(value["type"], "agent.tool_use");
assert_eq!(value["tool_use_id"], "tu_001");
assert_eq!(value["name"], "search");
assert_eq!(value["input"]["query"], "rust async");
assert_eq!(value["seq"], 2);
}
#[test]
fn test_session_custom_tool_use_serialization() {
let event = SessionEvent::CustomToolUse {
custom_tool_use_id: "ctu_001".to_string(),
name: "deploy".to_string(),
input: json!({"target": "production"}),
seq: 3,
};
let value = serde_json::to_value(&event).unwrap();
assert_eq!(value["type"], "agent.custom_tool_use");
assert_eq!(value["custom_tool_use_id"], "ctu_001");
assert_eq!(value["name"], "deploy");
assert_eq!(value["input"]["target"], "production");
assert_eq!(value["seq"], 3);
}
#[test]
fn test_session_mcp_tool_use_serialization() {
let event = SessionEvent::McpToolUse {
tool_use_id: "mcp_001".to_string(),
name: "file_read".to_string(),
input: json!({"path": "/tmp/data.txt"}),
seq: 4,
};
let value = serde_json::to_value(&event).unwrap();
assert_eq!(value["type"], "agent.mcp_tool_use");
assert_eq!(value["tool_use_id"], "mcp_001");
assert_eq!(value["name"], "file_read");
assert_eq!(value["input"]["path"], "/tmp/data.txt");
assert_eq!(value["seq"], 4);
}
#[test]
fn test_session_status_running_serialization() {
let event = SessionEvent::StatusRunning { seq: 5 };
let value = serde_json::to_value(&event).unwrap();
assert_eq!(value["type"], "status.running");
assert_eq!(value["seq"], 5);
}
#[test]
fn test_session_status_idle_serialization_no_stop_reason() {
let event = SessionEvent::StatusIdle { seq: 6, stop_reason: None, usage: None };
let value = serde_json::to_value(&event).unwrap();
assert_eq!(value["type"], "status.idle");
assert_eq!(value["seq"], 6);
assert_eq!(value["stop_reason"], json!(null));
}
#[test]
fn test_session_status_idle_with_end_turn() {
let event = SessionEvent::StatusIdle {
seq: 7,
stop_reason: Some(StopReason::EndTurn),
usage: None,
};
let value = serde_json::to_value(&event).unwrap();
assert_eq!(value["type"], "status.idle");
assert_eq!(value["seq"], 7);
assert_eq!(value["stop_reason"]["reason"], "end_turn");
}
#[test]
fn test_session_status_idle_with_requires_action() {
let event = SessionEvent::StatusIdle {
seq: 8,
stop_reason: Some(StopReason::RequiresAction {
event_ids: vec!["evt_001".to_string(), "evt_002".to_string()],
}),
usage: None,
};
let value = serde_json::to_value(&event).unwrap();
assert_eq!(value["type"], "status.idle");
assert_eq!(value["seq"], 8);
assert_eq!(value["stop_reason"]["reason"], "requires_action");
assert_eq!(value["stop_reason"]["event_ids"], json!(["evt_001", "evt_002"]));
}
#[test]
fn test_session_status_idle_with_max_tokens() {
let event = SessionEvent::StatusIdle {
seq: 9,
stop_reason: Some(StopReason::MaxTokens),
usage: None,
};
let value = serde_json::to_value(&event).unwrap();
assert_eq!(value["type"], "status.idle");
assert_eq!(value["seq"], 9);
assert_eq!(value["stop_reason"]["reason"], "max_tokens");
}
#[test]
fn test_session_error_serialization() {
let event = SessionEvent::Error {
code: "provider_error".to_string(),
message: "Model unavailable".to_string(),
seq: 10,
};
let value = serde_json::to_value(&event).unwrap();
assert_eq!(value["type"], "error");
assert_eq!(value["code"], "provider_error");
assert_eq!(value["message"], "Model unavailable");
assert_eq!(value["seq"], 10);
}
#[test]
fn test_session_event_seq_strictly_increasing() {
let events = vec![
SessionEvent::StatusRunning { seq: 0 },
SessionEvent::Message {
content: vec![ContentBlock::Text { text: "Hello".to_string() }],
seq: 1,
},
SessionEvent::ToolUse {
tool_use_id: "tu_1".to_string(),
name: "search".to_string(),
input: json!({}),
seq: 2,
},
SessionEvent::CustomToolUse {
custom_tool_use_id: "ctu_1".to_string(),
name: "deploy".to_string(),
input: json!({}),
seq: 3,
},
SessionEvent::McpToolUse {
tool_use_id: "mcp_1".to_string(),
name: "read".to_string(),
input: json!({}),
seq: 4,
},
SessionEvent::StatusIdle {
seq: 5,
stop_reason: Some(StopReason::EndTurn),
usage: None,
},
];
let seqs: Vec<u64> = events
.iter()
.map(|e| match e {
SessionEvent::StatusRunning { seq }
| SessionEvent::Message { seq, .. }
| SessionEvent::ToolUse { seq, .. }
| SessionEvent::CustomToolUse { seq, .. }
| SessionEvent::McpToolUse { seq, .. }
| SessionEvent::StatusIdle { seq, .. }
| SessionEvent::Error { seq, .. } => *seq,
})
.collect();
for window in seqs.windows(2) {
assert!(
window[1] > window[0],
"seq must be strictly increasing: {} should be > {}",
window[1],
window[0]
);
}
}
#[test]
fn test_session_event_round_trip() {
let event = SessionEvent::CustomToolUse {
custom_tool_use_id: "ctu_rt".to_string(),
name: "execute".to_string(),
input: json!({"command": "ls -la"}),
seq: 42,
};
let json = serde_json::to_string(&event).unwrap();
let deserialized: SessionEvent = serde_json::from_str(&json).unwrap();
match deserialized {
SessionEvent::CustomToolUse { custom_tool_use_id, name, input, seq } => {
assert_eq!(custom_tool_use_id, "ctu_rt");
assert_eq!(name, "execute");
assert_eq!(input["command"], "ls -la");
assert_eq!(seq, 42);
}
_ => panic!("Expected CustomToolUse variant"),
}
}
#[test]
fn test_stop_reason_end_turn_serialization() {
let reason = StopReason::EndTurn;
let value = serde_json::to_value(&reason).unwrap();
assert_eq!(value, json!({"reason": "end_turn"}));
}
#[test]
fn test_stop_reason_requires_action_serialization() {
let reason = StopReason::RequiresAction {
event_ids: vec!["evt_a".to_string(), "evt_b".to_string()],
};
let value = serde_json::to_value(&reason).unwrap();
assert_eq!(value, json!({"reason": "requires_action", "event_ids": ["evt_a", "evt_b"]}));
}
#[test]
fn test_stop_reason_max_tokens_serialization() {
let reason = StopReason::MaxTokens;
let value = serde_json::to_value(&reason).unwrap();
assert_eq!(value, json!({"reason": "max_tokens"}));
}
#[test]
fn test_stop_reason_round_trip() {
let reasons = vec![
StopReason::EndTurn,
StopReason::RequiresAction { event_ids: vec!["id1".to_string()] },
StopReason::MaxTokens,
];
for reason in reasons {
let json = serde_json::to_string(&reason).unwrap();
let deserialized: StopReason = serde_json::from_str(&json).unwrap();
let re_serialized = serde_json::to_string(&deserialized).unwrap();
assert_eq!(json, re_serialized);
}
}
}