use serde::{Deserialize, Serialize};
use crate::compaction::{CompactTrigger, CompactionResult, CompactionStrategy, TokenBudgetState};
use crate::tool::ToolOutput;
use crate::types::{AgentId, FinishReason, ModelId, SessionId, ToolCallId};
use crate::usage::Usage;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AgentEvent {
AgentStarted {
session_id: SessionId,
agent_name: String,
model_id: ModelId,
},
AgentEnded {
session_id: SessionId,
finish_reason: AgentFinishReason,
#[serde(skip_serializing_if = "Option::is_none")]
total_usage: Option<Usage>,
steps: u32,
},
StepStarted {
step_index: u32,
model_id: ModelId,
agent_name: String,
},
StepEnded {
step_index: u32,
finish_reason: FinishReason,
#[serde(skip_serializing_if = "Option::is_none")]
usage: Option<Usage>,
},
StepFailed {
step_index: u32,
error: String,
},
TextStarted {
content_index: usize,
},
TextDelta {
content_index: usize,
delta: String,
},
TextEnded {
content_index: usize,
text: String,
},
ReasoningStarted {
content_index: usize,
},
ReasoningDelta {
content_index: usize,
delta: String,
},
ReasoningEnded {
content_index: usize,
text: String,
},
ToolInputStarted {
call_id: ToolCallId,
tool_name: String,
},
ToolInputDelta {
call_id: ToolCallId,
delta: String,
},
ToolInputEnded {
call_id: ToolCallId,
arguments: serde_json::Value,
},
ToolCalled {
call_id: ToolCallId,
tool_name: String,
arguments: serde_json::Value,
},
ToolProgress {
call_id: ToolCallId,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<serde_json::Value>,
},
ToolSucceeded {
call_id: ToolCallId,
tool_name: String,
output: ToolOutput,
},
ToolFailed {
call_id: ToolCallId,
tool_name: String,
error: String,
#[serde(default)]
is_retryable: bool,
},
Retried {
attempt: u32,
error: String,
delay_ms: u64,
},
PruneCompleted {
tokens_freed: u64,
parts_pruned: usize,
},
CompactionStarted {
trigger: CompactTrigger,
strategy: CompactionStrategy,
tokens_before: u64,
},
CompactionDelta {
delta: String,
},
CompactionEnded {
result: CompactionResult,
},
TokenBudgetChanged {
used_tokens: u64,
context_window: u64,
state: TokenBudgetState,
},
ModelSwitched {
#[serde(skip_serializing_if = "Option::is_none")]
from: Option<ModelId>,
to: ModelId,
},
AgentSwitched {
#[serde(skip_serializing_if = "Option::is_none")]
from_agent: Option<AgentId>,
to_agent: AgentId,
agent_name: String,
},
UserPrompted {
content_preview: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AgentFinishReason {
Completed,
UserAbort,
MaxSteps,
TokenBudget,
Error,
Timeout,
}
impl std::fmt::Display for AgentFinishReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Completed => write!(f, "completed"),
Self::UserAbort => write!(f, "user_abort"),
Self::MaxSteps => write!(f, "max_steps"),
Self::TokenBudget => write!(f, "token_budget"),
Self::Error => write!(f, "error"),
Self::Timeout => write!(f, "timeout"),
}
}
}
impl AgentEvent {
pub fn kind(&self) -> AgentEventKind {
match self {
Self::AgentStarted { .. } => AgentEventKind::AgentStarted,
Self::AgentEnded { .. } => AgentEventKind::AgentEnded,
Self::StepStarted { .. } => AgentEventKind::StepStarted,
Self::StepEnded { .. } => AgentEventKind::StepEnded,
Self::StepFailed { .. } => AgentEventKind::StepFailed,
Self::TextStarted { .. } => AgentEventKind::TextStarted,
Self::TextDelta { .. } => AgentEventKind::TextDelta,
Self::TextEnded { .. } => AgentEventKind::TextEnded,
Self::ReasoningStarted { .. } => AgentEventKind::ReasoningStarted,
Self::ReasoningDelta { .. } => AgentEventKind::ReasoningDelta,
Self::ReasoningEnded { .. } => AgentEventKind::ReasoningEnded,
Self::ToolInputStarted { .. } => AgentEventKind::ToolInputStarted,
Self::ToolInputDelta { .. } => AgentEventKind::ToolInputDelta,
Self::ToolInputEnded { .. } => AgentEventKind::ToolInputEnded,
Self::ToolCalled { .. } => AgentEventKind::ToolCalled,
Self::ToolProgress { .. } => AgentEventKind::ToolProgress,
Self::ToolSucceeded { .. } => AgentEventKind::ToolSucceeded,
Self::ToolFailed { .. } => AgentEventKind::ToolFailed,
Self::Retried { .. } => AgentEventKind::Retried,
Self::PruneCompleted { .. } => AgentEventKind::PruneCompleted,
Self::CompactionStarted { .. } => AgentEventKind::CompactionStarted,
Self::CompactionDelta { .. } => AgentEventKind::CompactionDelta,
Self::CompactionEnded { .. } => AgentEventKind::CompactionEnded,
Self::TokenBudgetChanged { .. } => AgentEventKind::TokenBudgetChanged,
Self::ModelSwitched { .. } => AgentEventKind::ModelSwitched,
Self::AgentSwitched { .. } => AgentEventKind::AgentSwitched,
Self::UserPrompted { .. } => AgentEventKind::UserPrompted,
}
}
pub fn is_terminal(&self) -> bool {
matches!(self, Self::AgentEnded { .. })
}
pub fn is_text_delta(&self) -> bool {
matches!(self, Self::TextDelta { .. })
}
pub fn as_text_delta(&self) -> Option<&str> {
match self {
Self::TextDelta { delta, .. } => Some(delta.as_str()),
_ => None,
}
}
pub fn is_tool_event(&self) -> bool {
matches!(
self,
Self::ToolInputStarted { .. }
| Self::ToolInputDelta { .. }
| Self::ToolInputEnded { .. }
| Self::ToolCalled { .. }
| Self::ToolProgress { .. }
| Self::ToolSucceeded { .. }
| Self::ToolFailed { .. }
)
}
pub fn tool_call_id(&self) -> Option<&ToolCallId> {
match self {
Self::ToolInputStarted { call_id, .. }
| Self::ToolInputDelta { call_id, .. }
| Self::ToolInputEnded { call_id, .. }
| Self::ToolCalled { call_id, .. }
| Self::ToolProgress { call_id, .. }
| Self::ToolSucceeded { call_id, .. }
| Self::ToolFailed { call_id, .. } => Some(call_id),
_ => None,
}
}
pub fn is_delta(&self) -> bool {
matches!(
self,
Self::TextDelta { .. }
| Self::ReasoningDelta { .. }
| Self::ToolInputDelta { .. }
| Self::CompactionDelta { .. }
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AgentEventKind {
AgentStarted,
AgentEnded,
StepStarted,
StepEnded,
StepFailed,
TextStarted,
TextDelta,
TextEnded,
ReasoningStarted,
ReasoningDelta,
ReasoningEnded,
ToolInputStarted,
ToolInputDelta,
ToolInputEnded,
ToolCalled,
ToolProgress,
ToolSucceeded,
ToolFailed,
Retried,
PruneCompleted,
CompactionStarted,
CompactionDelta,
CompactionEnded,
TokenBudgetChanged,
ModelSwitched,
AgentSwitched,
UserPrompted,
}
impl std::fmt::Display for AgentEventKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::AgentStarted => "agent_started",
Self::AgentEnded => "agent_ended",
Self::StepStarted => "step_started",
Self::StepEnded => "step_ended",
Self::StepFailed => "step_failed",
Self::TextStarted => "text_started",
Self::TextDelta => "text_delta",
Self::TextEnded => "text_ended",
Self::ReasoningStarted => "reasoning_started",
Self::ReasoningDelta => "reasoning_delta",
Self::ReasoningEnded => "reasoning_ended",
Self::ToolInputStarted => "tool_input_started",
Self::ToolInputDelta => "tool_input_delta",
Self::ToolInputEnded => "tool_input_ended",
Self::ToolCalled => "tool_called",
Self::ToolProgress => "tool_progress",
Self::ToolSucceeded => "tool_succeeded",
Self::ToolFailed => "tool_failed",
Self::Retried => "retried",
Self::PruneCompleted => "prune_completed",
Self::CompactionStarted => "compaction_started",
Self::CompactionDelta => "compaction_delta",
Self::CompactionEnded => "compaction_ended",
Self::TokenBudgetChanged => "token_budget_changed",
Self::ModelSwitched => "model_switched",
Self::AgentSwitched => "agent_switched",
Self::UserPrompted => "user_prompted",
};
write!(f, "{s}")
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_agent_finish_reason_serde_roundtrip() {
let reasons = [
AgentFinishReason::Completed,
AgentFinishReason::UserAbort,
AgentFinishReason::MaxSteps,
AgentFinishReason::TokenBudget,
AgentFinishReason::Error,
AgentFinishReason::Timeout,
];
for reason in &reasons {
let json = serde_json::to_string(reason).unwrap();
let restored: AgentFinishReason = serde_json::from_str(&json).unwrap();
assert_eq!(reason, &restored);
}
}
#[test]
fn test_agent_finish_reason_display() {
assert_eq!(AgentFinishReason::Completed.to_string(), "completed");
assert_eq!(AgentFinishReason::UserAbort.to_string(), "user_abort");
assert_eq!(AgentFinishReason::MaxSteps.to_string(), "max_steps");
}
#[test]
fn test_compact_trigger_serde() {
for trigger in [
CompactTrigger::Auto,
CompactTrigger::Manual,
CompactTrigger::Overflow,
CompactTrigger::Idle,
] {
let json = serde_json::to_string(&trigger).unwrap();
let restored: CompactTrigger = serde_json::from_str(&json).unwrap();
assert_eq!(trigger, restored);
}
}
#[test]
fn test_agent_started_serde() {
let event = AgentEvent::AgentStarted {
session_id: SessionId::new(),
agent_name: "coder".into(),
model_id: ModelId::new("gpt-4o"),
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains(r#""type":"agent_started""#));
assert!(json.contains(r#""agent_name":"coder""#));
let restored: AgentEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, restored);
}
#[test]
fn test_agent_ended_serde() {
let event = AgentEvent::AgentEnded {
session_id: SessionId::new(),
finish_reason: AgentFinishReason::Completed,
total_usage: None,
steps: 3,
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains(r#""type":"agent_ended""#));
assert!(!json.contains("total_usage"));
assert!(json.contains(r#""steps":3"#));
let restored: AgentEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, restored);
}
#[test]
fn test_agent_ended_with_usage() {
let event = AgentEvent::AgentEnded {
session_id: SessionId::new(),
finish_reason: AgentFinishReason::MaxSteps,
total_usage: Some(Usage {
input_tokens: 1000,
output_tokens: 500,
total_tokens: 1500,
..Default::default()
}),
steps: 10,
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("total_usage"));
let restored: AgentEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, restored);
}
#[test]
fn test_step_started_serde() {
let event = AgentEvent::StepStarted {
step_index: 0,
model_id: ModelId::new("claude-sonnet-4-20250514"),
agent_name: "default".into(),
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains(r#""type":"step_started""#));
let restored: AgentEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, restored);
}
#[test]
fn test_step_ended_serde() {
let event = AgentEvent::StepEnded {
step_index: 0,
finish_reason: FinishReason::Stop,
usage: None,
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains(r#""type":"step_ended""#));
let restored: AgentEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, restored);
}
#[test]
fn test_step_failed_serde() {
let event = AgentEvent::StepFailed {
step_index: 2,
error: "rate limit exceeded".into(),
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains(r#""type":"step_failed""#));
let restored: AgentEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, restored);
}
#[test]
fn test_text_lifecycle_serde() {
let started = AgentEvent::TextStarted { content_index: 0 };
let delta = AgentEvent::TextDelta {
content_index: 0,
delta: "Hello ".into(),
};
let ended = AgentEvent::TextEnded {
content_index: 0,
text: "Hello world".into(),
};
for event in [&started, &delta, &ended] {
let json = serde_json::to_string(event).unwrap();
let restored: AgentEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, &restored);
}
}
#[test]
fn test_reasoning_lifecycle_serde() {
let started = AgentEvent::ReasoningStarted { content_index: 1 };
let delta = AgentEvent::ReasoningDelta {
content_index: 1,
delta: "Let me think...".into(),
};
let ended = AgentEvent::ReasoningEnded {
content_index: 1,
text: "Let me think about this carefully.".into(),
};
for event in [&started, &delta, &ended] {
let json = serde_json::to_string(event).unwrap();
let restored: AgentEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, &restored);
}
}
#[test]
fn test_tool_input_lifecycle_serde() {
let call_id = ToolCallId::new("call_abc123");
let started = AgentEvent::ToolInputStarted {
call_id: call_id.clone(),
tool_name: "read_file".into(),
};
let delta = AgentEvent::ToolInputDelta {
call_id: call_id.clone(),
delta: r#"{"path": "src/"#.into(),
};
let ended = AgentEvent::ToolInputEnded {
call_id: call_id.clone(),
arguments: json!({"path": "src/main.rs"}),
};
for event in [&started, &delta, &ended] {
let json = serde_json::to_string(event).unwrap();
let restored: AgentEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, &restored);
}
}
#[test]
fn test_tool_called_serde() {
let event = AgentEvent::ToolCalled {
call_id: ToolCallId::new("call_1"),
tool_name: "bash".into(),
arguments: json!({"command": "ls -la"}),
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains(r#""type":"tool_called""#));
let restored: AgentEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, restored);
}
#[test]
fn test_tool_progress_serde() {
let event = AgentEvent::ToolProgress {
call_id: ToolCallId::new("call_1"),
message: "Reading file...".into(),
data: Some(json!({"bytes_read": 1024})),
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains(r#""type":"tool_progress""#));
let restored: AgentEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, restored);
}
#[test]
fn test_tool_progress_no_data_serde() {
let event = AgentEvent::ToolProgress {
call_id: ToolCallId::new("call_1"),
message: "Working...".into(),
data: None,
};
let json = serde_json::to_string(&event).unwrap();
assert!(!json.contains(r#""data""#));
let restored: AgentEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, restored);
}
#[test]
fn test_tool_succeeded_serde() {
let event = AgentEvent::ToolSucceeded {
call_id: ToolCallId::new("call_1"),
tool_name: "read_file".into(),
output: ToolOutput::success("file contents here"),
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains(r#""type":"tool_succeeded""#));
let restored: AgentEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, restored);
}
#[test]
fn test_tool_failed_serde() {
let event = AgentEvent::ToolFailed {
call_id: ToolCallId::new("call_2"),
tool_name: "write_file".into(),
error: "permission denied".into(),
is_retryable: false,
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains(r#""type":"tool_failed""#));
let restored: AgentEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, restored);
}
#[test]
fn test_tool_failed_retryable_default() {
let json = r#"{
"type":"tool_failed",
"call_id":"call_x",
"tool_name":"bash",
"error":"timeout"
}"#;
let event: AgentEvent = serde_json::from_str(json).unwrap();
if let AgentEvent::ToolFailed { is_retryable, .. } = event {
assert!(!is_retryable);
} else {
panic!("expected ToolFailed");
}
}
#[test]
fn test_retried_serde() {
let event = AgentEvent::Retried {
attempt: 2,
error: "rate limit".into(),
delay_ms: 5000,
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains(r#""type":"retried""#));
let restored: AgentEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, restored);
}
#[test]
fn test_prune_completed_serde() {
let event = AgentEvent::PruneCompleted {
tokens_freed: 25_000,
parts_pruned: 12,
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains(r#""type":"prune_completed""#));
let restored: AgentEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, restored);
}
#[test]
fn test_compaction_lifecycle_serde() {
let started = AgentEvent::CompactionStarted {
trigger: CompactTrigger::Auto,
strategy: CompactionStrategy::Summarize,
tokens_before: 150_000,
};
let delta = AgentEvent::CompactionDelta {
delta: "Summary: ...".into(),
};
let ended = AgentEvent::CompactionEnded {
result: CompactionResult {
summary: "The user asked about Rust error handling...".into(),
short_summary: Some("Rust error handling".into()),
trigger: CompactTrigger::Auto,
tokens_before: 150_000,
tokens_after: Some(5_000),
messages_compacted: 40,
messages_kept: 8,
success: true,
},
};
for event in [&started, &delta, &ended] {
let json = serde_json::to_string(event).unwrap();
let restored: AgentEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, &restored);
}
}
#[test]
fn test_token_budget_changed_serde() {
let event = AgentEvent::TokenBudgetChanged {
used_tokens: 170_000,
context_window: 200_000,
state: TokenBudgetState::Warning {
percent_remaining: 0.15,
},
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains(r#""type":"token_budget_changed""#));
let restored: AgentEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, restored);
}
#[test]
fn test_model_switched_serde() {
let event = AgentEvent::ModelSwitched {
from: Some(ModelId::new("gpt-4o")),
to: ModelId::new("claude-sonnet-4-20250514"),
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains(r#""type":"model_switched""#));
let restored: AgentEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, restored);
}
#[test]
fn test_model_switched_no_from_serde() {
let event = AgentEvent::ModelSwitched {
from: None,
to: ModelId::new("gpt-4o"),
};
let json = serde_json::to_string(&event).unwrap();
assert!(!json.contains(r#""from""#));
let restored: AgentEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, restored);
}
#[test]
fn test_agent_switched_serde() {
let event = AgentEvent::AgentSwitched {
from_agent: None,
to_agent: AgentId::new(),
agent_name: "code-reviewer".into(),
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains(r#""type":"agent_switched""#));
let restored: AgentEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, restored);
}
#[test]
fn test_user_prompted_serde() {
let event = AgentEvent::UserPrompted {
content_preview: "Fix the bug in main.rs".into(),
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains(r#""type":"user_prompted""#));
let restored: AgentEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, restored);
}
#[test]
fn test_agent_event_kind_display() {
assert_eq!(AgentEventKind::AgentStarted.to_string(), "agent_started");
assert_eq!(AgentEventKind::ToolCalled.to_string(), "tool_called");
assert_eq!(
AgentEventKind::CompactionEnded.to_string(),
"compaction_ended"
);
}
#[test]
fn test_kind_method() {
let event = AgentEvent::TextDelta {
content_index: 0,
delta: "hi".into(),
};
assert_eq!(event.kind(), AgentEventKind::TextDelta);
let event = AgentEvent::ToolCalled {
call_id: ToolCallId::new("c"),
tool_name: "t".into(),
arguments: json!({}),
};
assert_eq!(event.kind(), AgentEventKind::ToolCalled);
}
#[test]
fn test_is_terminal() {
let ended = AgentEvent::AgentEnded {
session_id: SessionId::new(),
finish_reason: AgentFinishReason::Completed,
total_usage: None,
steps: 1,
};
assert!(ended.is_terminal());
let started = AgentEvent::AgentStarted {
session_id: SessionId::new(),
agent_name: "a".into(),
model_id: ModelId::new("m"),
};
assert!(!started.is_terminal());
}
#[test]
fn test_is_text_delta() {
let delta = AgentEvent::TextDelta {
content_index: 0,
delta: "hello".into(),
};
assert!(delta.is_text_delta());
assert_eq!(delta.as_text_delta(), Some("hello"));
let other = AgentEvent::ReasoningDelta {
content_index: 0,
delta: "think".into(),
};
assert!(!other.is_text_delta());
assert_eq!(other.as_text_delta(), None);
}
#[test]
fn test_is_tool_event() {
let tool = AgentEvent::ToolCalled {
call_id: ToolCallId::new("c"),
tool_name: "t".into(),
arguments: json!({}),
};
assert!(tool.is_tool_event());
assert_eq!(tool.tool_call_id(), Some(&ToolCallId::new("c")));
let text = AgentEvent::TextDelta {
content_index: 0,
delta: "hi".into(),
};
assert!(!text.is_tool_event());
assert_eq!(text.tool_call_id(), None);
}
#[test]
fn test_is_delta() {
assert!(AgentEvent::TextDelta {
content_index: 0,
delta: "a".into()
}
.is_delta());
assert!(AgentEvent::ReasoningDelta {
content_index: 0,
delta: "b".into()
}
.is_delta());
assert!(AgentEvent::ToolInputDelta {
call_id: ToolCallId::new("c"),
delta: "d".into()
}
.is_delta());
assert!(AgentEvent::CompactionDelta {
delta: "e".into()
}
.is_delta());
assert!(!AgentEvent::TextStarted { content_index: 0 }.is_delta());
assert!(!AgentEvent::ToolCalled {
call_id: ToolCallId::new("c"),
tool_name: "t".into(),
arguments: json!({}),
}
.is_delta());
}
#[test]
fn test_tool_call_id_all_tool_events() {
let id = ToolCallId::new("test_id");
let events = vec![
AgentEvent::ToolInputStarted {
call_id: id.clone(),
tool_name: "t".into(),
},
AgentEvent::ToolInputDelta {
call_id: id.clone(),
delta: "d".into(),
},
AgentEvent::ToolInputEnded {
call_id: id.clone(),
arguments: json!({}),
},
AgentEvent::ToolCalled {
call_id: id.clone(),
tool_name: "t".into(),
arguments: json!({}),
},
AgentEvent::ToolProgress {
call_id: id.clone(),
message: "m".into(),
data: None,
},
AgentEvent::ToolSucceeded {
call_id: id.clone(),
tool_name: "t".into(),
output: ToolOutput::success("ok"),
},
AgentEvent::ToolFailed {
call_id: id.clone(),
tool_name: "t".into(),
error: "e".into(),
is_retryable: false,
},
];
for event in &events {
assert_eq!(
event.tool_call_id(),
Some(&id),
"tool_call_id() failed for {:?}",
event.kind()
);
}
}
}