Skip to main content

katu_core/
agent_event.rs

1//! # katu_core::agent_event
2//!
3//! ## 职责
4//! 定义 Agent 语义层事件 — Agent loop 在执行过程中产出的已发生事件。
5//!
6//! ## 设计
7//! `AgentEvent` 是 **StreamEvent 的上层抽象**:
8//! - `StreamEvent`(event.rs)= LLM provider 层的原始 SSE 事件
9//! - `AgentEvent`(本模块)= Agent loop 语义层的已发生事实
10//!
11//! Agent loop 消费 `StreamEvent` 流,经过工具执行、状态管理等逻辑后,
12//! 产出 `AgentEvent` 供 UI/日志/持久化层订阅。
13//!
14//! ## 事件分类(28 种)
15//! 按 namespace 分 8 组:
16//! - **Agent** — 整个 Agent 运行的开始/结束
17//! - **Step** — 单次 LLM 推理步骤的生命周期
18//! - **Text** — 文本流式输出
19//! - **Reasoning** — 思维链流式输出
20//! - **Tool** — 工具从参数生成到执行完成的全生命周期
21//! - **Retry** — API 调用重试
22//! - **Compaction** — 上下文压缩(含修剪和 token 预算变更)
23//! - **State** — 配置/状态变更
24//!
25//! ## 与 StreamEvent 的关系
26//! ```text
27//! LLM Provider ──SSE──► StreamEvent ──Agent Loop──► AgentEvent ──► UI / 日志
28//! ```
29//!
30//! ## 调用者
31//! - `katu-agent` (future) — Agent loop 产出 AgentEvent
32//! - UI 层 — 实时渲染 Agent 执行状态
33//! - 持久化层 — 记录执行历史
34//! - 遥测层 — 统计和监控
35
36use serde::{Deserialize, Serialize};
37
38use crate::compaction::{CompactTrigger, CompactionResult, CompactionStrategy, TokenBudgetState};
39use crate::tool::ToolOutput;
40use crate::types::{AgentId, FinishReason, ModelId, SessionId, ToolCallId};
41use crate::usage::Usage;
42
43// ===========================================================================
44// AgentEvent
45// ===========================================================================
46
47/// Agent 语义层事件 — Agent loop 在执行过程中产出的已发生事实。
48///
49/// 事件是 **不可变的已发生事实**,消费者只能观察,不能拦截或修改。
50/// 如需拦截/修改 Agent 行为,请使用 Hook 系统(future)。
51///
52/// # 事件分层
53///
54/// ```text
55/// AgentStarted
56/// ├── StepStarted
57/// │   ├── TextStarted → TextDelta* → TextEnded
58/// │   ├── ReasoningStarted → ReasoningDelta* → ReasoningEnded
59/// │   ├── ToolInputStarted → ToolInputDelta* → ToolInputEnded
60/// │   │   └── ToolCalled → ToolProgress* → ToolSucceeded / ToolFailed
61/// │   └── StepEnded / StepFailed
62/// ├── Retried
63/// ├── PruneCompleted
64/// ├── CompactionStarted → CompactionDelta* → CompactionEnded
65/// ├── TokenBudgetChanged
66/// ├── ModelSwitched / AgentSwitched / UserPrompted
67/// └── AgentEnded
68/// ```
69///
70/// # Serde 格式
71///
72/// 所有事件序列化为 `{"type": "snake_case_variant", ...fields}` 格式,
73/// 与 `StreamEvent` 保持风格一致。
74///
75/// # Examples
76///
77/// ```
78/// use katu_core::agent_event::AgentEvent;
79/// use katu_core::{SessionId, ModelId};
80///
81/// let event = AgentEvent::AgentStarted {
82///     session_id: SessionId::new(),
83///     agent_name: "coder".into(),
84///     model_id: ModelId::new("gpt-4o"),
85/// };
86/// let json = serde_json::to_string(&event).unwrap();
87/// assert!(json.contains(r#""type":"agent_started""#));
88/// ```
89#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
90#[serde(tag = "type", rename_all = "snake_case")]
91pub enum AgentEvent {
92    // ── 1. Agent 生命周期 (2) ────────────────────────────────
93
94    /// Agent loop 开始执行。
95    AgentStarted {
96        session_id: SessionId,
97        agent_name: String,
98        model_id: ModelId,
99    },
100
101    /// Agent loop 执行结束。
102    AgentEnded {
103        session_id: SessionId,
104        finish_reason: AgentFinishReason,
105        /// 整个 Agent 运行的累计 token 用量。
106        #[serde(skip_serializing_if = "Option::is_none")]
107        total_usage: Option<Usage>,
108        /// 总执行步数。
109        steps: u32,
110    },
111
112    // ── 2. Step 生命周期 (3) ─────────────────────────────────
113
114    /// 一个推理步骤开始(一次 LLM API 调用)。
115    StepStarted {
116        step_index: u32,
117        model_id: ModelId,
118        agent_name: String,
119    },
120
121    /// 一个推理步骤正常完成。
122    StepEnded {
123        step_index: u32,
124        finish_reason: FinishReason,
125        /// 本步骤的 token 用量。
126        #[serde(skip_serializing_if = "Option::is_none")]
127        usage: Option<Usage>,
128    },
129
130    /// 一个推理步骤失败(API 错误、网络错误等)。
131    StepFailed {
132        step_index: u32,
133        error: String,
134    },
135
136    // ── 3. Text 流式 (3) ────────────────────────────────────
137
138    /// 文本内容块开始。
139    TextStarted {
140        content_index: usize,
141    },
142
143    /// 文本增量。
144    TextDelta {
145        content_index: usize,
146        delta: String,
147    },
148
149    /// 文本内容块完成,携带完整文本。
150    TextEnded {
151        content_index: usize,
152        text: String,
153    },
154
155    // ── 4. Reasoning 流式 (3) ───────────────────────────────
156
157    /// 思维链内容块开始。
158    ReasoningStarted {
159        content_index: usize,
160    },
161
162    /// 思维链增量。
163    ReasoningDelta {
164        content_index: usize,
165        delta: String,
166    },
167
168    /// 思维链内容块完成,携带完整文本。
169    ReasoningEnded {
170        content_index: usize,
171        text: String,
172    },
173
174    // ── 5. Tool 生命周期 (7) ────────────────────────────────
175
176    /// LLM 开始生成工具调用参数。
177    ToolInputStarted {
178        call_id: ToolCallId,
179        tool_name: String,
180    },
181
182    /// 工具参数 JSON 增量。
183    ToolInputDelta {
184        call_id: ToolCallId,
185        delta: String,
186    },
187
188    /// 工具参数生成完毕,携带完整参数。
189    ToolInputEnded {
190        call_id: ToolCallId,
191        arguments: serde_json::Value,
192    },
193
194    /// 工具开始执行(参数已解析、权限已通过)。
195    ToolCalled {
196        call_id: ToolCallId,
197        tool_name: String,
198        arguments: serde_json::Value,
199    },
200
201    /// 工具执行中间进度。
202    ToolProgress {
203        call_id: ToolCallId,
204        /// 进度消息(如 "正在读取文件...")。
205        message: String,
206        /// 可选的结构化进度数据。
207        #[serde(skip_serializing_if = "Option::is_none")]
208        data: Option<serde_json::Value>,
209    },
210
211    /// 工具执行成功。
212    ToolSucceeded {
213        call_id: ToolCallId,
214        tool_name: String,
215        output: ToolOutput,
216    },
217
218    /// 工具执行失败。
219    ToolFailed {
220        call_id: ToolCallId,
221        tool_name: String,
222        error: String,
223        /// 是否可重试。
224        #[serde(default)]
225        is_retryable: bool,
226    },
227
228    // ── 6. Retry (1) ────────────────────────────────────────
229
230    /// API 调用重试。
231    Retried {
232        /// 当前重试次数(从 1 开始)。
233        attempt: u32,
234        /// 触发重试的错误。
235        error: String,
236        /// 重试间隔(毫秒)。
237        delay_ms: u64,
238    },
239
240    // ── 7. Compaction (5) ───────────────────────────────────
241
242    /// 旧工具输出修剪完成。
243    ///
244    /// Prune 是轻量级的上下文优化,独立于全量压缩。
245    /// 截断旧的、体积大的工具输出,释放 token 空间。
246    PruneCompleted {
247        /// 修剪释放的估计 token 数。
248        tokens_freed: u64,
249        /// 被修剪的工具输出条数。
250        parts_pruned: usize,
251    },
252
253    /// 上下文压缩开始。
254    CompactionStarted {
255        /// 触发原因。
256        trigger: CompactTrigger,
257        /// 使用的压缩策略。
258        strategy: CompactionStrategy,
259        /// 压缩前的 prompt token 数。
260        tokens_before: u64,
261    },
262
263    /// 压缩摘要增量。
264    CompactionDelta {
265        delta: String,
266    },
267
268    /// 上下文压缩完成。
269    CompactionEnded {
270        /// 压缩完整结果。
271        result: CompactionResult,
272    },
273
274    /// Token 用量预算状态变更。
275    ///
276    /// 当 token 用量跨越阈值边界时产出,用于 UI 进度条渲染。
277    TokenBudgetChanged {
278        /// 当前已使用 token 数。
279        used_tokens: u64,
280        /// 模型 context window 大小。
281        context_window: u64,
282        /// 新状态。
283        state: TokenBudgetState,
284    },
285
286    // ── 8. State 变更 (3) ───────────────────────────────────
287
288    /// 模型已切换。
289    ModelSwitched {
290        #[serde(skip_serializing_if = "Option::is_none")]
291        from: Option<ModelId>,
292        to: ModelId,
293    },
294
295    /// Agent 已切换(进入或退出子 Agent)。
296    AgentSwitched {
297        #[serde(skip_serializing_if = "Option::is_none")]
298        from_agent: Option<AgentId>,
299        to_agent: AgentId,
300        agent_name: String,
301    },
302
303    /// 用户提交了新 prompt。
304    UserPrompted {
305        /// 用户输入内容的摘要(可能截断,避免事件过大)。
306        content_preview: String,
307    },
308}
309
310// ===========================================================================
311// 辅助枚举
312// ===========================================================================
313
314/// Agent 运行结束原因。
315///
316/// 区别于 `FinishReason`(LLM 单步停止原因),`AgentFinishReason`
317/// 描述整个 Agent loop 的终止原因。
318///
319/// # Examples
320///
321/// ```
322/// use katu_core::agent_event::AgentFinishReason;
323///
324/// let reason = AgentFinishReason::Completed;
325/// let json = serde_json::to_string(&reason).unwrap();
326/// assert_eq!(json, r#""completed""#);
327/// ```
328#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
329#[serde(rename_all = "snake_case")]
330pub enum AgentFinishReason {
331    /// 正常完成 — LLM 在最终步骤返回 end_turn 且无待处理的 tool call。
332    Completed,
333    /// 用户中断。
334    UserAbort,
335    /// 达到最大步数限制。
336    MaxSteps,
337    /// 达到 token 预算上限。
338    TokenBudget,
339    /// 不可恢复错误导致终止。
340    Error,
341    /// 超时。
342    Timeout,
343}
344
345impl std::fmt::Display for AgentFinishReason {
346    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
347        match self {
348            Self::Completed => write!(f, "completed"),
349            Self::UserAbort => write!(f, "user_abort"),
350            Self::MaxSteps => write!(f, "max_steps"),
351            Self::TokenBudget => write!(f, "token_budget"),
352            Self::Error => write!(f, "error"),
353            Self::Timeout => write!(f, "timeout"),
354        }
355    }
356}
357
358// ===========================================================================
359// AgentEvent — helper methods
360// ===========================================================================
361
362impl AgentEvent {
363    /// 返回此事件的类型标签。
364    pub fn kind(&self) -> AgentEventKind {
365        match self {
366            Self::AgentStarted { .. } => AgentEventKind::AgentStarted,
367            Self::AgentEnded { .. } => AgentEventKind::AgentEnded,
368            Self::StepStarted { .. } => AgentEventKind::StepStarted,
369            Self::StepEnded { .. } => AgentEventKind::StepEnded,
370            Self::StepFailed { .. } => AgentEventKind::StepFailed,
371            Self::TextStarted { .. } => AgentEventKind::TextStarted,
372            Self::TextDelta { .. } => AgentEventKind::TextDelta,
373            Self::TextEnded { .. } => AgentEventKind::TextEnded,
374            Self::ReasoningStarted { .. } => AgentEventKind::ReasoningStarted,
375            Self::ReasoningDelta { .. } => AgentEventKind::ReasoningDelta,
376            Self::ReasoningEnded { .. } => AgentEventKind::ReasoningEnded,
377            Self::ToolInputStarted { .. } => AgentEventKind::ToolInputStarted,
378            Self::ToolInputDelta { .. } => AgentEventKind::ToolInputDelta,
379            Self::ToolInputEnded { .. } => AgentEventKind::ToolInputEnded,
380            Self::ToolCalled { .. } => AgentEventKind::ToolCalled,
381            Self::ToolProgress { .. } => AgentEventKind::ToolProgress,
382            Self::ToolSucceeded { .. } => AgentEventKind::ToolSucceeded,
383            Self::ToolFailed { .. } => AgentEventKind::ToolFailed,
384            Self::Retried { .. } => AgentEventKind::Retried,
385            Self::PruneCompleted { .. } => AgentEventKind::PruneCompleted,
386            Self::CompactionStarted { .. } => AgentEventKind::CompactionStarted,
387            Self::CompactionDelta { .. } => AgentEventKind::CompactionDelta,
388            Self::CompactionEnded { .. } => AgentEventKind::CompactionEnded,
389            Self::TokenBudgetChanged { .. } => AgentEventKind::TokenBudgetChanged,
390            Self::ModelSwitched { .. } => AgentEventKind::ModelSwitched,
391            Self::AgentSwitched { .. } => AgentEventKind::AgentSwitched,
392            Self::UserPrompted { .. } => AgentEventKind::UserPrompted,
393        }
394    }
395
396    /// 是否为终态事件(AgentEnded)。
397    pub fn is_terminal(&self) -> bool {
398        matches!(self, Self::AgentEnded { .. })
399    }
400
401    /// 是否为文本增量事件。
402    pub fn is_text_delta(&self) -> bool {
403        matches!(self, Self::TextDelta { .. })
404    }
405
406    /// 提取文本增量内容,非 TextDelta 事件返回 None。
407    pub fn as_text_delta(&self) -> Option<&str> {
408        match self {
409            Self::TextDelta { delta, .. } => Some(delta.as_str()),
410            _ => None,
411        }
412    }
413
414    /// 是否为工具生命周期事件。
415    pub fn is_tool_event(&self) -> bool {
416        matches!(
417            self,
418            Self::ToolInputStarted { .. }
419                | Self::ToolInputDelta { .. }
420                | Self::ToolInputEnded { .. }
421                | Self::ToolCalled { .. }
422                | Self::ToolProgress { .. }
423                | Self::ToolSucceeded { .. }
424                | Self::ToolFailed { .. }
425        )
426    }
427
428    /// 提取关联的 ToolCallId(如果是工具事件)。
429    pub fn tool_call_id(&self) -> Option<&ToolCallId> {
430        match self {
431            Self::ToolInputStarted { call_id, .. }
432            | Self::ToolInputDelta { call_id, .. }
433            | Self::ToolInputEnded { call_id, .. }
434            | Self::ToolCalled { call_id, .. }
435            | Self::ToolProgress { call_id, .. }
436            | Self::ToolSucceeded { call_id, .. }
437            | Self::ToolFailed { call_id, .. } => Some(call_id),
438            _ => None,
439        }
440    }
441
442    /// 是否为 delta 类事件(高频、增量)。
443    pub fn is_delta(&self) -> bool {
444        matches!(
445            self,
446            Self::TextDelta { .. }
447                | Self::ReasoningDelta { .. }
448                | Self::ToolInputDelta { .. }
449                | Self::CompactionDelta { .. }
450        )
451    }
452}
453
454// ===========================================================================
455// AgentEventKind — 判别标签
456// ===========================================================================
457
458/// AgentEvent 的类型判别标签(无数据),用于过滤和分类。
459///
460/// 与 `AgentEvent` 一一对应,可用于:
461/// - 事件过滤器(只订阅关心的事件类型)
462/// - 统计计数
463/// - Hook 系统的事件匹配
464#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
465#[serde(rename_all = "snake_case")]
466pub enum AgentEventKind {
467    // Agent
468    AgentStarted,
469    AgentEnded,
470    // Step
471    StepStarted,
472    StepEnded,
473    StepFailed,
474    // Text
475    TextStarted,
476    TextDelta,
477    TextEnded,
478    // Reasoning
479    ReasoningStarted,
480    ReasoningDelta,
481    ReasoningEnded,
482    // Tool
483    ToolInputStarted,
484    ToolInputDelta,
485    ToolInputEnded,
486    ToolCalled,
487    ToolProgress,
488    ToolSucceeded,
489    ToolFailed,
490    // Retry
491    Retried,
492    // Compaction
493    PruneCompleted,
494    CompactionStarted,
495    CompactionDelta,
496    CompactionEnded,
497    TokenBudgetChanged,
498    // State
499    ModelSwitched,
500    AgentSwitched,
501    UserPrompted,
502}
503
504impl std::fmt::Display for AgentEventKind {
505    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
506        let s = match self {
507            Self::AgentStarted => "agent_started",
508            Self::AgentEnded => "agent_ended",
509            Self::StepStarted => "step_started",
510            Self::StepEnded => "step_ended",
511            Self::StepFailed => "step_failed",
512            Self::TextStarted => "text_started",
513            Self::TextDelta => "text_delta",
514            Self::TextEnded => "text_ended",
515            Self::ReasoningStarted => "reasoning_started",
516            Self::ReasoningDelta => "reasoning_delta",
517            Self::ReasoningEnded => "reasoning_ended",
518            Self::ToolInputStarted => "tool_input_started",
519            Self::ToolInputDelta => "tool_input_delta",
520            Self::ToolInputEnded => "tool_input_ended",
521            Self::ToolCalled => "tool_called",
522            Self::ToolProgress => "tool_progress",
523            Self::ToolSucceeded => "tool_succeeded",
524            Self::ToolFailed => "tool_failed",
525            Self::Retried => "retried",
526            Self::PruneCompleted => "prune_completed",
527            Self::CompactionStarted => "compaction_started",
528            Self::CompactionDelta => "compaction_delta",
529            Self::CompactionEnded => "compaction_ended",
530            Self::TokenBudgetChanged => "token_budget_changed",
531            Self::ModelSwitched => "model_switched",
532            Self::AgentSwitched => "agent_switched",
533            Self::UserPrompted => "user_prompted",
534        };
535        write!(f, "{s}")
536    }
537}
538
539
540// ===========================================================================
541// Tests
542// ===========================================================================
543
544#[cfg(test)]
545mod tests {
546    use super::*;
547    use serde_json::json;
548
549    // -- AgentFinishReason --
550
551    #[test]
552    fn test_agent_finish_reason_serde_roundtrip() {
553        let reasons = [
554            AgentFinishReason::Completed,
555            AgentFinishReason::UserAbort,
556            AgentFinishReason::MaxSteps,
557            AgentFinishReason::TokenBudget,
558            AgentFinishReason::Error,
559            AgentFinishReason::Timeout,
560        ];
561        for reason in &reasons {
562            let json = serde_json::to_string(reason).unwrap();
563            let restored: AgentFinishReason = serde_json::from_str(&json).unwrap();
564            assert_eq!(reason, &restored);
565        }
566    }
567
568    #[test]
569    fn test_agent_finish_reason_display() {
570        assert_eq!(AgentFinishReason::Completed.to_string(), "completed");
571        assert_eq!(AgentFinishReason::UserAbort.to_string(), "user_abort");
572        assert_eq!(AgentFinishReason::MaxSteps.to_string(), "max_steps");
573    }
574
575    // -- CompactTrigger (from compaction module) --
576
577    #[test]
578    fn test_compact_trigger_serde() {
579        for trigger in [
580            CompactTrigger::Auto,
581            CompactTrigger::Manual,
582            CompactTrigger::Overflow,
583            CompactTrigger::Idle,
584        ] {
585            let json = serde_json::to_string(&trigger).unwrap();
586            let restored: CompactTrigger = serde_json::from_str(&json).unwrap();
587            assert_eq!(trigger, restored);
588        }
589    }
590
591    // -- AgentEvent serde --
592
593    #[test]
594    fn test_agent_started_serde() {
595        let event = AgentEvent::AgentStarted {
596            session_id: SessionId::new(),
597            agent_name: "coder".into(),
598            model_id: ModelId::new("gpt-4o"),
599        };
600        let json = serde_json::to_string(&event).unwrap();
601        assert!(json.contains(r#""type":"agent_started""#));
602        assert!(json.contains(r#""agent_name":"coder""#));
603        let restored: AgentEvent = serde_json::from_str(&json).unwrap();
604        assert_eq!(event, restored);
605    }
606
607    #[test]
608    fn test_agent_ended_serde() {
609        let event = AgentEvent::AgentEnded {
610            session_id: SessionId::new(),
611            finish_reason: AgentFinishReason::Completed,
612            total_usage: None,
613            steps: 3,
614        };
615        let json = serde_json::to_string(&event).unwrap();
616        assert!(json.contains(r#""type":"agent_ended""#));
617        assert!(!json.contains("total_usage"));
618        assert!(json.contains(r#""steps":3"#));
619        let restored: AgentEvent = serde_json::from_str(&json).unwrap();
620        assert_eq!(event, restored);
621    }
622
623    #[test]
624    fn test_agent_ended_with_usage() {
625        let event = AgentEvent::AgentEnded {
626            session_id: SessionId::new(),
627            finish_reason: AgentFinishReason::MaxSteps,
628            total_usage: Some(Usage {
629                input_tokens: 1000,
630                output_tokens: 500,
631                total_tokens: 1500,
632                ..Default::default()
633            }),
634            steps: 10,
635        };
636        let json = serde_json::to_string(&event).unwrap();
637        assert!(json.contains("total_usage"));
638        let restored: AgentEvent = serde_json::from_str(&json).unwrap();
639        assert_eq!(event, restored);
640    }
641
642    // -- Step events --
643
644    #[test]
645    fn test_step_started_serde() {
646        let event = AgentEvent::StepStarted {
647            step_index: 0,
648            model_id: ModelId::new("claude-sonnet-4-20250514"),
649            agent_name: "default".into(),
650        };
651        let json = serde_json::to_string(&event).unwrap();
652        assert!(json.contains(r#""type":"step_started""#));
653        let restored: AgentEvent = serde_json::from_str(&json).unwrap();
654        assert_eq!(event, restored);
655    }
656
657    #[test]
658    fn test_step_ended_serde() {
659        let event = AgentEvent::StepEnded {
660            step_index: 0,
661            finish_reason: FinishReason::Stop,
662            usage: None,
663        };
664        let json = serde_json::to_string(&event).unwrap();
665        assert!(json.contains(r#""type":"step_ended""#));
666        let restored: AgentEvent = serde_json::from_str(&json).unwrap();
667        assert_eq!(event, restored);
668    }
669
670    #[test]
671    fn test_step_failed_serde() {
672        let event = AgentEvent::StepFailed {
673            step_index: 2,
674            error: "rate limit exceeded".into(),
675        };
676        let json = serde_json::to_string(&event).unwrap();
677        assert!(json.contains(r#""type":"step_failed""#));
678        let restored: AgentEvent = serde_json::from_str(&json).unwrap();
679        assert_eq!(event, restored);
680    }
681
682    // -- Text events --
683
684    #[test]
685    fn test_text_lifecycle_serde() {
686        let started = AgentEvent::TextStarted { content_index: 0 };
687        let delta = AgentEvent::TextDelta {
688            content_index: 0,
689            delta: "Hello ".into(),
690        };
691        let ended = AgentEvent::TextEnded {
692            content_index: 0,
693            text: "Hello world".into(),
694        };
695        for event in [&started, &delta, &ended] {
696            let json = serde_json::to_string(event).unwrap();
697            let restored: AgentEvent = serde_json::from_str(&json).unwrap();
698            assert_eq!(event, &restored);
699        }
700    }
701
702    // -- Reasoning events --
703
704    #[test]
705    fn test_reasoning_lifecycle_serde() {
706        let started = AgentEvent::ReasoningStarted { content_index: 1 };
707        let delta = AgentEvent::ReasoningDelta {
708            content_index: 1,
709            delta: "Let me think...".into(),
710        };
711        let ended = AgentEvent::ReasoningEnded {
712            content_index: 1,
713            text: "Let me think about this carefully.".into(),
714        };
715        for event in [&started, &delta, &ended] {
716            let json = serde_json::to_string(event).unwrap();
717            let restored: AgentEvent = serde_json::from_str(&json).unwrap();
718            assert_eq!(event, &restored);
719        }
720    }
721
722    // -- Tool events --
723
724    #[test]
725    fn test_tool_input_lifecycle_serde() {
726        let call_id = ToolCallId::new("call_abc123");
727
728        let started = AgentEvent::ToolInputStarted {
729            call_id: call_id.clone(),
730            tool_name: "read_file".into(),
731        };
732        let delta = AgentEvent::ToolInputDelta {
733            call_id: call_id.clone(),
734            delta: r#"{"path": "src/"#.into(),
735        };
736        let ended = AgentEvent::ToolInputEnded {
737            call_id: call_id.clone(),
738            arguments: json!({"path": "src/main.rs"}),
739        };
740
741        for event in [&started, &delta, &ended] {
742            let json = serde_json::to_string(event).unwrap();
743            let restored: AgentEvent = serde_json::from_str(&json).unwrap();
744            assert_eq!(event, &restored);
745        }
746    }
747
748    #[test]
749    fn test_tool_called_serde() {
750        let event = AgentEvent::ToolCalled {
751            call_id: ToolCallId::new("call_1"),
752            tool_name: "bash".into(),
753            arguments: json!({"command": "ls -la"}),
754        };
755        let json = serde_json::to_string(&event).unwrap();
756        assert!(json.contains(r#""type":"tool_called""#));
757        let restored: AgentEvent = serde_json::from_str(&json).unwrap();
758        assert_eq!(event, restored);
759    }
760
761    #[test]
762    fn test_tool_progress_serde() {
763        let event = AgentEvent::ToolProgress {
764            call_id: ToolCallId::new("call_1"),
765            message: "Reading file...".into(),
766            data: Some(json!({"bytes_read": 1024})),
767        };
768        let json = serde_json::to_string(&event).unwrap();
769        assert!(json.contains(r#""type":"tool_progress""#));
770        let restored: AgentEvent = serde_json::from_str(&json).unwrap();
771        assert_eq!(event, restored);
772    }
773
774    #[test]
775    fn test_tool_progress_no_data_serde() {
776        let event = AgentEvent::ToolProgress {
777            call_id: ToolCallId::new("call_1"),
778            message: "Working...".into(),
779            data: None,
780        };
781        let json = serde_json::to_string(&event).unwrap();
782        assert!(!json.contains(r#""data""#));
783        let restored: AgentEvent = serde_json::from_str(&json).unwrap();
784        assert_eq!(event, restored);
785    }
786
787    #[test]
788    fn test_tool_succeeded_serde() {
789        let event = AgentEvent::ToolSucceeded {
790            call_id: ToolCallId::new("call_1"),
791            tool_name: "read_file".into(),
792            output: ToolOutput::success("file contents here"),
793        };
794        let json = serde_json::to_string(&event).unwrap();
795        assert!(json.contains(r#""type":"tool_succeeded""#));
796        let restored: AgentEvent = serde_json::from_str(&json).unwrap();
797        assert_eq!(event, restored);
798    }
799
800    #[test]
801    fn test_tool_failed_serde() {
802        let event = AgentEvent::ToolFailed {
803            call_id: ToolCallId::new("call_2"),
804            tool_name: "write_file".into(),
805            error: "permission denied".into(),
806            is_retryable: false,
807        };
808        let json = serde_json::to_string(&event).unwrap();
809        assert!(json.contains(r#""type":"tool_failed""#));
810        let restored: AgentEvent = serde_json::from_str(&json).unwrap();
811        assert_eq!(event, restored);
812    }
813
814    #[test]
815    fn test_tool_failed_retryable_default() {
816        let json = r#"{
817            "type":"tool_failed",
818            "call_id":"call_x",
819            "tool_name":"bash",
820            "error":"timeout"
821        }"#;
822        let event: AgentEvent = serde_json::from_str(json).unwrap();
823        if let AgentEvent::ToolFailed { is_retryable, .. } = event {
824            assert!(!is_retryable);
825        } else {
826            panic!("expected ToolFailed");
827        }
828    }
829
830    // -- Retry --
831
832    #[test]
833    fn test_retried_serde() {
834        let event = AgentEvent::Retried {
835            attempt: 2,
836            error: "rate limit".into(),
837            delay_ms: 5000,
838        };
839        let json = serde_json::to_string(&event).unwrap();
840        assert!(json.contains(r#""type":"retried""#));
841        let restored: AgentEvent = serde_json::from_str(&json).unwrap();
842        assert_eq!(event, restored);
843    }
844
845    // -- Compaction --
846
847    #[test]
848    fn test_prune_completed_serde() {
849        let event = AgentEvent::PruneCompleted {
850            tokens_freed: 25_000,
851            parts_pruned: 12,
852        };
853        let json = serde_json::to_string(&event).unwrap();
854        assert!(json.contains(r#""type":"prune_completed""#));
855        let restored: AgentEvent = serde_json::from_str(&json).unwrap();
856        assert_eq!(event, restored);
857    }
858
859    #[test]
860    fn test_compaction_lifecycle_serde() {
861        let started = AgentEvent::CompactionStarted {
862            trigger: CompactTrigger::Auto,
863            strategy: CompactionStrategy::Summarize,
864            tokens_before: 150_000,
865        };
866        let delta = AgentEvent::CompactionDelta {
867            delta: "Summary: ...".into(),
868        };
869        let ended = AgentEvent::CompactionEnded {
870            result: CompactionResult {
871                summary: "The user asked about Rust error handling...".into(),
872                short_summary: Some("Rust error handling".into()),
873                trigger: CompactTrigger::Auto,
874                tokens_before: 150_000,
875                tokens_after: Some(5_000),
876                messages_compacted: 40,
877                messages_kept: 8,
878                success: true,
879            },
880        };
881        for event in [&started, &delta, &ended] {
882            let json = serde_json::to_string(event).unwrap();
883            let restored: AgentEvent = serde_json::from_str(&json).unwrap();
884            assert_eq!(event, &restored);
885        }
886    }
887
888    #[test]
889    fn test_token_budget_changed_serde() {
890        let event = AgentEvent::TokenBudgetChanged {
891            used_tokens: 170_000,
892            context_window: 200_000,
893            state: TokenBudgetState::Warning {
894                percent_remaining: 0.15,
895            },
896        };
897        let json = serde_json::to_string(&event).unwrap();
898        assert!(json.contains(r#""type":"token_budget_changed""#));
899        let restored: AgentEvent = serde_json::from_str(&json).unwrap();
900        assert_eq!(event, restored);
901    }
902
903    // -- State --
904
905    #[test]
906    fn test_model_switched_serde() {
907        let event = AgentEvent::ModelSwitched {
908            from: Some(ModelId::new("gpt-4o")),
909            to: ModelId::new("claude-sonnet-4-20250514"),
910        };
911        let json = serde_json::to_string(&event).unwrap();
912        assert!(json.contains(r#""type":"model_switched""#));
913        let restored: AgentEvent = serde_json::from_str(&json).unwrap();
914        assert_eq!(event, restored);
915    }
916
917    #[test]
918    fn test_model_switched_no_from_serde() {
919        let event = AgentEvent::ModelSwitched {
920            from: None,
921            to: ModelId::new("gpt-4o"),
922        };
923        let json = serde_json::to_string(&event).unwrap();
924        assert!(!json.contains(r#""from""#));
925        let restored: AgentEvent = serde_json::from_str(&json).unwrap();
926        assert_eq!(event, restored);
927    }
928
929    #[test]
930    fn test_agent_switched_serde() {
931        let event = AgentEvent::AgentSwitched {
932            from_agent: None,
933            to_agent: AgentId::new(),
934            agent_name: "code-reviewer".into(),
935        };
936        let json = serde_json::to_string(&event).unwrap();
937        assert!(json.contains(r#""type":"agent_switched""#));
938        let restored: AgentEvent = serde_json::from_str(&json).unwrap();
939        assert_eq!(event, restored);
940    }
941
942    #[test]
943    fn test_user_prompted_serde() {
944        let event = AgentEvent::UserPrompted {
945            content_preview: "Fix the bug in main.rs".into(),
946        };
947        let json = serde_json::to_string(&event).unwrap();
948        assert!(json.contains(r#""type":"user_prompted""#));
949        let restored: AgentEvent = serde_json::from_str(&json).unwrap();
950        assert_eq!(event, restored);
951    }
952
953    // -- AgentEventKind --
954
955    #[test]
956    fn test_agent_event_kind_display() {
957        assert_eq!(AgentEventKind::AgentStarted.to_string(), "agent_started");
958        assert_eq!(AgentEventKind::ToolCalled.to_string(), "tool_called");
959        assert_eq!(
960            AgentEventKind::CompactionEnded.to_string(),
961            "compaction_ended"
962        );
963    }
964
965
966    // -- Helper methods --
967
968    #[test]
969    fn test_kind_method() {
970        let event = AgentEvent::TextDelta {
971            content_index: 0,
972            delta: "hi".into(),
973        };
974        assert_eq!(event.kind(), AgentEventKind::TextDelta);
975
976        let event = AgentEvent::ToolCalled {
977            call_id: ToolCallId::new("c"),
978            tool_name: "t".into(),
979            arguments: json!({}),
980        };
981        assert_eq!(event.kind(), AgentEventKind::ToolCalled);
982    }
983
984    #[test]
985    fn test_is_terminal() {
986        let ended = AgentEvent::AgentEnded {
987            session_id: SessionId::new(),
988            finish_reason: AgentFinishReason::Completed,
989            total_usage: None,
990            steps: 1,
991        };
992        assert!(ended.is_terminal());
993
994        let started = AgentEvent::AgentStarted {
995            session_id: SessionId::new(),
996            agent_name: "a".into(),
997            model_id: ModelId::new("m"),
998        };
999        assert!(!started.is_terminal());
1000    }
1001
1002    #[test]
1003    fn test_is_text_delta() {
1004        let delta = AgentEvent::TextDelta {
1005            content_index: 0,
1006            delta: "hello".into(),
1007        };
1008        assert!(delta.is_text_delta());
1009        assert_eq!(delta.as_text_delta(), Some("hello"));
1010
1011        let other = AgentEvent::ReasoningDelta {
1012            content_index: 0,
1013            delta: "think".into(),
1014        };
1015        assert!(!other.is_text_delta());
1016        assert_eq!(other.as_text_delta(), None);
1017    }
1018
1019    #[test]
1020    fn test_is_tool_event() {
1021        let tool = AgentEvent::ToolCalled {
1022            call_id: ToolCallId::new("c"),
1023            tool_name: "t".into(),
1024            arguments: json!({}),
1025        };
1026        assert!(tool.is_tool_event());
1027        assert_eq!(tool.tool_call_id(), Some(&ToolCallId::new("c")));
1028
1029        let text = AgentEvent::TextDelta {
1030            content_index: 0,
1031            delta: "hi".into(),
1032        };
1033        assert!(!text.is_tool_event());
1034        assert_eq!(text.tool_call_id(), None);
1035    }
1036
1037    #[test]
1038    fn test_is_delta() {
1039        assert!(AgentEvent::TextDelta {
1040            content_index: 0,
1041            delta: "a".into()
1042        }
1043        .is_delta());
1044        assert!(AgentEvent::ReasoningDelta {
1045            content_index: 0,
1046            delta: "b".into()
1047        }
1048        .is_delta());
1049        assert!(AgentEvent::ToolInputDelta {
1050            call_id: ToolCallId::new("c"),
1051            delta: "d".into()
1052        }
1053        .is_delta());
1054        assert!(AgentEvent::CompactionDelta {
1055            delta: "e".into()
1056        }
1057        .is_delta());
1058
1059        assert!(!AgentEvent::TextStarted { content_index: 0 }.is_delta());
1060        assert!(!AgentEvent::ToolCalled {
1061            call_id: ToolCallId::new("c"),
1062            tool_name: "t".into(),
1063            arguments: json!({}),
1064        }
1065        .is_delta());
1066    }
1067
1068    #[test]
1069    fn test_tool_call_id_all_tool_events() {
1070        let id = ToolCallId::new("test_id");
1071        let events = vec![
1072            AgentEvent::ToolInputStarted {
1073                call_id: id.clone(),
1074                tool_name: "t".into(),
1075            },
1076            AgentEvent::ToolInputDelta {
1077                call_id: id.clone(),
1078                delta: "d".into(),
1079            },
1080            AgentEvent::ToolInputEnded {
1081                call_id: id.clone(),
1082                arguments: json!({}),
1083            },
1084            AgentEvent::ToolCalled {
1085                call_id: id.clone(),
1086                tool_name: "t".into(),
1087                arguments: json!({}),
1088            },
1089            AgentEvent::ToolProgress {
1090                call_id: id.clone(),
1091                message: "m".into(),
1092                data: None,
1093            },
1094            AgentEvent::ToolSucceeded {
1095                call_id: id.clone(),
1096                tool_name: "t".into(),
1097                output: ToolOutput::success("ok"),
1098            },
1099            AgentEvent::ToolFailed {
1100                call_id: id.clone(),
1101                tool_name: "t".into(),
1102                error: "e".into(),
1103                is_retryable: false,
1104            },
1105        ];
1106        for event in &events {
1107            assert_eq!(
1108                event.tool_call_id(),
1109                Some(&id),
1110                "tool_call_id() failed for {:?}",
1111                event.kind()
1112            );
1113        }
1114    }
1115}