Skip to main content

aios_protocol/
event.rs

1//! Canonical event types for the Agent OS.
2//!
3//! Merges the best of three event models:
4//! - Lago's `EventPayload` (35+ variants, forward-compatible deserializer)
5//! - Arcan's `AgentEvent` (24 variants, runtime/streaming focused)
6//! - aiOS's `EventKind` (40+ variants, homeostasis/voice/phases)
7//!
8//! Forward-compatible: unknown `"type"` tags deserialize into
9//! `Custom { event_type, data }` instead of failing.
10
11use crate::ids::*;
12use crate::memory::MemoryScope;
13use crate::mode::OperatingMode;
14use crate::state::{AgentStateVector, BudgetState, StatePatch};
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17
18/// Event actor identity.
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
20#[serde(rename_all = "snake_case")]
21pub enum ActorType {
22    User,
23    Agent,
24    System,
25}
26
27/// Event actor metadata.
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
29pub struct EventActor {
30    #[serde(rename = "type")]
31    pub actor_type: ActorType,
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub component: Option<String>,
34}
35
36impl Default for EventActor {
37    fn default() -> Self {
38        Self {
39            actor_type: ActorType::System,
40            component: Some("arcan-daemon".to_owned()),
41        }
42    }
43}
44
45/// Event schema descriptor.
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
47pub struct EventSchema {
48    pub name: String,
49    pub version: String,
50}
51
52impl Default for EventSchema {
53    fn default() -> Self {
54        Self {
55            name: "aios-protocol".to_owned(),
56            version: "0.1.0".to_owned(),
57        }
58    }
59}
60
61fn default_agent_id() -> AgentId {
62    AgentId::default()
63}
64
65/// The universal state-change envelope for the Agent OS.
66///
67/// Adopts Lago's structure: typed IDs, branch-aware sequencing,
68/// causal links, metadata bag, and schema versioning.
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct EventEnvelope {
71    pub event_id: EventId,
72    pub session_id: SessionId,
73    #[serde(default = "default_agent_id")]
74    pub agent_id: AgentId,
75    pub branch_id: BranchId,
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub run_id: Option<RunId>,
78    pub seq: SeqNo,
79    /// Microseconds since UNIX epoch.
80    #[serde(rename = "ts_ms", alias = "timestamp")]
81    pub timestamp: u64,
82    #[serde(default)]
83    pub actor: EventActor,
84    #[serde(default)]
85    pub schema: EventSchema,
86    #[serde(skip_serializing_if = "Option::is_none")]
87    #[serde(rename = "parent_event_id", alias = "parent_id")]
88    pub parent_id: Option<EventId>,
89    #[serde(default, skip_serializing_if = "Option::is_none")]
90    pub trace_id: Option<String>,
91    #[serde(default, skip_serializing_if = "Option::is_none")]
92    pub span_id: Option<String>,
93    #[serde(default, skip_serializing_if = "Option::is_none")]
94    pub digest: Option<String>,
95    pub kind: EventKind,
96    #[serde(default)]
97    pub metadata: HashMap<String, String>,
98    #[serde(default = "default_schema_version")]
99    pub schema_version: u8,
100}
101
102fn default_schema_version() -> u8 {
103    1
104}
105
106impl EventEnvelope {
107    /// Current time in microseconds since UNIX epoch.
108    pub fn now_micros() -> u64 {
109        std::time::SystemTime::now()
110            .duration_since(std::time::UNIX_EPOCH)
111            .unwrap_or_default()
112            .as_micros() as u64
113    }
114}
115
116/// Convenience event record using `chrono::DateTime<Utc>` timestamps.
117///
118/// This is the type used by aiOS internal crates. It maps to `EventEnvelope`
119/// for storage/streaming but uses ergonomic Rust types for construction.
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct EventRecord {
122    pub event_id: EventId,
123    pub session_id: SessionId,
124    #[serde(default = "default_agent_id")]
125    pub agent_id: AgentId,
126    pub branch_id: BranchId,
127    pub sequence: SeqNo,
128    pub timestamp: chrono::DateTime<chrono::Utc>,
129    #[serde(default)]
130    pub actor: EventActor,
131    #[serde(default)]
132    pub schema: EventSchema,
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub causation_id: Option<EventId>,
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub correlation_id: Option<String>,
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub trace_id: Option<String>,
139    #[serde(default, skip_serializing_if = "Option::is_none")]
140    pub span_id: Option<String>,
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub digest: Option<String>,
143    pub kind: EventKind,
144}
145
146impl EventRecord {
147    /// Create a new event record with the current timestamp.
148    pub fn new(
149        session_id: SessionId,
150        branch_id: BranchId,
151        sequence: SeqNo,
152        kind: EventKind,
153    ) -> Self {
154        Self {
155            event_id: EventId::default(),
156            session_id,
157            agent_id: AgentId::default(),
158            branch_id,
159            sequence,
160            timestamp: chrono::Utc::now(),
161            actor: EventActor::default(),
162            schema: EventSchema::default(),
163            causation_id: None,
164            correlation_id: None,
165            trace_id: None,
166            span_id: None,
167            digest: None,
168            kind,
169        }
170    }
171
172    /// Convert to the canonical `EventEnvelope` for storage/streaming.
173    pub fn to_envelope(&self) -> EventEnvelope {
174        EventEnvelope {
175            event_id: self.event_id.clone(),
176            session_id: self.session_id.clone(),
177            agent_id: self.agent_id.clone(),
178            branch_id: self.branch_id.clone(),
179            run_id: None,
180            seq: self.sequence,
181            timestamp: self.timestamp.timestamp_micros() as u64,
182            actor: self.actor.clone(),
183            schema: self.schema.clone(),
184            parent_id: self.causation_id.clone(),
185            trace_id: self.trace_id.clone(),
186            span_id: self.span_id.clone(),
187            digest: self.digest.clone(),
188            kind: self.kind.clone(),
189            metadata: HashMap::new(),
190            schema_version: 1,
191        }
192    }
193}
194
195// ─── Canonical EventKind ───────────────────────────────────────────
196
197/// Discriminated union of ALL Agent OS event types.
198///
199/// This is the canonical taxonomy that all projects (Arcan, Lago, aiOS,
200/// Autonomic) must use. Merges ~55 variants from three separate models.
201///
202/// Forward-compatible: unknown `"type"` tags deserialize into `Custom`.
203#[derive(Debug, Clone, Serialize)]
204#[non_exhaustive]
205#[serde(tag = "type")]
206pub enum EventKind {
207    // ── Input / sensing ──
208    UserMessage {
209        content: String,
210    },
211    ExternalSignal {
212        signal_type: String,
213        data: serde_json::Value,
214    },
215
216    // ── Session lifecycle ──
217    SessionCreated {
218        name: String,
219        config: serde_json::Value,
220    },
221    SessionResumed {
222        #[serde(skip_serializing_if = "Option::is_none")]
223        from_snapshot: Option<SnapshotId>,
224    },
225    SessionClosed {
226        reason: String,
227    },
228
229    // ── Branch lifecycle ──
230    BranchCreated {
231        new_branch_id: BranchId,
232        fork_point_seq: SeqNo,
233        name: String,
234    },
235    BranchMerged {
236        source_branch_id: BranchId,
237        merge_seq: SeqNo,
238    },
239
240    // ── Loop phases (from aiOS) ──
241    PhaseEntered {
242        phase: LoopPhase,
243    },
244    DeliberationProposed {
245        summary: String,
246        #[serde(skip_serializing_if = "Option::is_none")]
247        proposed_tool: Option<String>,
248    },
249
250    // ── Run lifecycle (from Lago + Arcan) ──
251    RunStarted {
252        provider: String,
253        max_iterations: u32,
254    },
255    RunFinished {
256        reason: String,
257        total_iterations: u32,
258        #[serde(skip_serializing_if = "Option::is_none")]
259        final_answer: Option<String>,
260        #[serde(skip_serializing_if = "Option::is_none")]
261        usage: Option<TokenUsage>,
262    },
263    RunErrored {
264        error: String,
265    },
266
267    // ── Step lifecycle (from Lago) ──
268    StepStarted {
269        index: u32,
270    },
271    StepFinished {
272        index: u32,
273        stop_reason: String,
274        directive_count: usize,
275    },
276
277    // ── Text streaming (from Arcan + Lago) ──
278    AssistantTextDelta {
279        delta: String,
280        #[serde(skip_serializing_if = "Option::is_none")]
281        index: Option<u32>,
282    },
283    AssistantMessageCommitted {
284        role: String,
285        content: String,
286        #[serde(skip_serializing_if = "Option::is_none")]
287        model: Option<String>,
288        #[serde(skip_serializing_if = "Option::is_none")]
289        token_usage: Option<TokenUsage>,
290    },
291    TextDelta {
292        delta: String,
293        #[serde(skip_serializing_if = "Option::is_none")]
294        index: Option<u32>,
295    },
296    Message {
297        role: String,
298        content: String,
299        #[serde(skip_serializing_if = "Option::is_none")]
300        model: Option<String>,
301        #[serde(skip_serializing_if = "Option::is_none")]
302        token_usage: Option<TokenUsage>,
303    },
304
305    // ── Tool lifecycle (merged from all three) ──
306    ToolCallRequested {
307        call_id: String,
308        tool_name: String,
309        arguments: serde_json::Value,
310        #[serde(skip_serializing_if = "Option::is_none")]
311        category: Option<String>,
312    },
313    ToolCallStarted {
314        tool_run_id: ToolRunId,
315        tool_name: String,
316    },
317    ToolCallCompleted {
318        tool_run_id: ToolRunId,
319        #[serde(skip_serializing_if = "Option::is_none")]
320        call_id: Option<String>,
321        tool_name: String,
322        result: serde_json::Value,
323        duration_ms: u64,
324        status: SpanStatus,
325    },
326    ToolCallFailed {
327        call_id: String,
328        tool_name: String,
329        error: String,
330    },
331
332    // ── File operations (from Lago) ──
333    FileWrite {
334        path: String,
335        blob_hash: BlobHash,
336        size_bytes: u64,
337        #[serde(skip_serializing_if = "Option::is_none")]
338        content_type: Option<String>,
339    },
340    FileDelete {
341        path: String,
342    },
343    FileRename {
344        old_path: String,
345        new_path: String,
346    },
347    FileMutated {
348        path: String,
349        content_hash: String,
350    },
351
352    // ── State management (from Lago + Arcan) ──
353    StatePatchCommitted {
354        new_version: u64,
355        patch: StatePatch,
356    },
357    StatePatched {
358        #[serde(skip_serializing_if = "Option::is_none")]
359        index: Option<u32>,
360        patch: serde_json::Value,
361        revision: u64,
362    },
363    ContextCompacted {
364        dropped_count: usize,
365        tokens_before: usize,
366        tokens_after: usize,
367    },
368
369    // ── Policy (from Lago) ──
370    PolicyEvaluated {
371        tool_name: String,
372        decision: PolicyDecisionKind,
373        #[serde(skip_serializing_if = "Option::is_none")]
374        rule_id: Option<String>,
375        #[serde(skip_serializing_if = "Option::is_none")]
376        explanation: Option<String>,
377    },
378
379    // ── Approval gate (from Lago + Arcan + aiOS) ──
380    ApprovalRequested {
381        approval_id: ApprovalId,
382        call_id: String,
383        tool_name: String,
384        arguments: serde_json::Value,
385        risk: RiskLevel,
386    },
387    ApprovalResolved {
388        approval_id: ApprovalId,
389        decision: ApprovalDecision,
390        #[serde(skip_serializing_if = "Option::is_none")]
391        reason: Option<String>,
392    },
393
394    // ── Snapshots (from Lago) ──
395    SnapshotCreated {
396        snapshot_id: SnapshotId,
397        snapshot_type: SnapshotType,
398        covers_through_seq: SeqNo,
399        data_hash: BlobHash,
400    },
401
402    // ── Sandbox lifecycle (from Lago) ──
403    SandboxCreated {
404        sandbox_id: String,
405        tier: String,
406        config: serde_json::Value,
407    },
408    SandboxExecuted {
409        sandbox_id: String,
410        command: String,
411        exit_code: i32,
412        duration_ms: u64,
413    },
414    SandboxViolation {
415        sandbox_id: String,
416        violation_type: String,
417        details: String,
418    },
419    SandboxDestroyed {
420        sandbox_id: String,
421    },
422
423    // ── Memory (from Lago) ──
424    ObservationAppended {
425        scope: MemoryScope,
426        observation_ref: BlobHash,
427        #[serde(skip_serializing_if = "Option::is_none")]
428        source_run_id: Option<String>,
429    },
430    ReflectionCompacted {
431        scope: MemoryScope,
432        summary_ref: BlobHash,
433        covers_through_seq: SeqNo,
434    },
435    MemoryProposed {
436        scope: MemoryScope,
437        proposal_id: MemoryId,
438        entries_ref: BlobHash,
439        #[serde(skip_serializing_if = "Option::is_none")]
440        source_run_id: Option<String>,
441    },
442    MemoryCommitted {
443        scope: MemoryScope,
444        memory_id: MemoryId,
445        committed_ref: BlobHash,
446        #[serde(skip_serializing_if = "Option::is_none")]
447        supersedes: Option<MemoryId>,
448    },
449    MemoryTombstoned {
450        scope: MemoryScope,
451        memory_id: MemoryId,
452        reason: String,
453    },
454
455    // ── Homeostasis (from aiOS) ──
456    Heartbeat {
457        summary: String,
458        #[serde(skip_serializing_if = "Option::is_none")]
459        checkpoint_id: Option<CheckpointId>,
460    },
461    StateEstimated {
462        state: AgentStateVector,
463        mode: OperatingMode,
464    },
465    BudgetUpdated {
466        budget: BudgetState,
467        reason: String,
468    },
469    ModeChanged {
470        from: OperatingMode,
471        to: OperatingMode,
472        reason: String,
473    },
474    GatesUpdated {
475        gates: serde_json::Value,
476        reason: String,
477    },
478    CircuitBreakerTripped {
479        reason: String,
480        error_streak: u32,
481    },
482
483    // ── Checkpoints (from aiOS) ──
484    CheckpointCreated {
485        checkpoint_id: CheckpointId,
486        event_sequence: u64,
487        state_hash: String,
488    },
489    CheckpointRestored {
490        checkpoint_id: CheckpointId,
491        restored_to_seq: u64,
492    },
493
494    // ── Voice (from aiOS) ──
495    VoiceSessionStarted {
496        voice_session_id: String,
497        adapter: String,
498        model: String,
499        sample_rate_hz: u32,
500        channels: u8,
501    },
502    VoiceInputChunk {
503        voice_session_id: String,
504        chunk_index: u64,
505        bytes: usize,
506        format: String,
507    },
508    VoiceOutputChunk {
509        voice_session_id: String,
510        chunk_index: u64,
511        bytes: usize,
512        format: String,
513    },
514    VoiceSessionStopped {
515        voice_session_id: String,
516        reason: String,
517    },
518    VoiceAdapterError {
519        voice_session_id: String,
520        message: String,
521    },
522
523    // ── World models (new, forward-looking) ──
524    WorldModelObserved {
525        state_ref: BlobHash,
526        meta: serde_json::Value,
527    },
528    WorldModelRollout {
529        trajectory_ref: BlobHash,
530        #[serde(skip_serializing_if = "Option::is_none")]
531        score: Option<f32>,
532    },
533
534    // ── Intent lifecycle (new) ──
535    IntentProposed {
536        intent_id: String,
537        kind: String,
538        #[serde(skip_serializing_if = "Option::is_none")]
539        risk: Option<RiskLevel>,
540    },
541    IntentEvaluated {
542        intent_id: String,
543        allowed: bool,
544        requires_approval: bool,
545        #[serde(default)]
546        reasons: Vec<String>,
547    },
548    IntentApproved {
549        intent_id: String,
550        #[serde(default, skip_serializing_if = "Option::is_none")]
551        actor: Option<String>,
552    },
553    IntentRejected {
554        intent_id: String,
555        #[serde(default)]
556        reasons: Vec<String>,
557    },
558
559    // ── Hive collaborative evolution ──
560    HiveTaskCreated {
561        hive_task_id: HiveTaskId,
562        objective: String,
563        agent_count: u32,
564    },
565    HiveArtifactShared {
566        hive_task_id: HiveTaskId,
567        source_session_id: SessionId,
568        score: f32,
569        mutation_summary: String,
570    },
571    HiveSelectionMade {
572        hive_task_id: HiveTaskId,
573        winning_session_id: SessionId,
574        winning_score: f32,
575        generation: u32,
576    },
577    HiveGenerationCompleted {
578        hive_task_id: HiveTaskId,
579        generation: u32,
580        best_score: f32,
581        agent_results: serde_json::Value,
582    },
583    HiveTaskCompleted {
584        hive_task_id: HiveTaskId,
585        total_generations: u32,
586        total_trials: u32,
587        final_score: f32,
588    },
589
590    // ── Queue & steering (Phase 2.5) ──
591    Queued {
592        queue_id: String,
593        mode: SteeringMode,
594        message: String,
595    },
596    Steered {
597        queue_id: String,
598        /// Tool boundary where preemption occurred (e.g. "tool:read_file:call-3").
599        preempted_at: String,
600    },
601    QueueDrained {
602        queue_id: String,
603        processed: usize,
604    },
605
606    // ── Error ──
607    ErrorRaised {
608        message: String,
609    },
610
611    // ── Forward-compatible catch-all ──
612    Custom {
613        event_type: String,
614        data: serde_json::Value,
615    },
616}
617
618// ─── Supporting types ──────────────────────────────────────────────
619
620/// Agent loop phase (from aiOS).
621#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
622#[serde(rename_all = "snake_case")]
623pub enum LoopPhase {
624    Perceive,
625    Deliberate,
626    Gate,
627    Execute,
628    Commit,
629    Reflect,
630    Sleep,
631}
632
633/// Token usage reported by LLM providers.
634#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
635pub struct TokenUsage {
636    #[serde(default)]
637    pub prompt_tokens: u32,
638    #[serde(default)]
639    pub completion_tokens: u32,
640    #[serde(default)]
641    pub total_tokens: u32,
642}
643
644/// Tool execution span status.
645#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
646#[serde(rename_all = "snake_case")]
647pub enum SpanStatus {
648    Ok,
649    Error,
650    Timeout,
651    Cancelled,
652}
653
654/// Risk level for policy evaluation. Includes Critical from Lago.
655#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
656#[serde(rename_all = "snake_case")]
657pub enum RiskLevel {
658    Low,
659    Medium,
660    High,
661    Critical,
662}
663
664/// Approval decision outcome.
665#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
666#[serde(rename_all = "snake_case")]
667pub enum ApprovalDecision {
668    Approved,
669    Denied,
670    Timeout,
671}
672
673/// Snapshot type.
674#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
675#[serde(rename_all = "snake_case")]
676pub enum SnapshotType {
677    Full,
678    Incremental,
679}
680
681/// Policy decision kind.
682#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
683#[serde(rename_all = "snake_case")]
684pub enum PolicyDecisionKind {
685    Allow,
686    Deny,
687    RequireApproval,
688}
689
690/// Steering mode for queued messages (Phase 2.5).
691///
692/// Determines how a queued message interacts with an active run.
693#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
694#[serde(rename_all = "snake_case")]
695pub enum SteeringMode {
696    /// Queue message for processing after current run completes.
697    Collect,
698    /// Redirect agent at next tool boundary (safe preemption).
699    Steer,
700    /// Queue as follow-up to current run (same context).
701    Followup,
702    /// Interrupt at next safe point (tool boundary), highest priority.
703    Interrupt,
704}
705
706// ─── Forward-compatible deserializer ───────────────────────────────
707
708/// Internal helper enum for the forward-compatible deserializer.
709/// Mirrors EventKind exactly but derives Deserialize.
710#[derive(Deserialize)]
711#[serde(tag = "type")]
712enum EventKindKnown {
713    UserMessage {
714        content: String,
715    },
716    ExternalSignal {
717        signal_type: String,
718        data: serde_json::Value,
719    },
720    SessionCreated {
721        name: String,
722        config: serde_json::Value,
723    },
724    SessionResumed {
725        #[serde(default)]
726        from_snapshot: Option<SnapshotId>,
727    },
728    SessionClosed {
729        reason: String,
730    },
731    BranchCreated {
732        new_branch_id: BranchId,
733        fork_point_seq: SeqNo,
734        name: String,
735    },
736    BranchMerged {
737        source_branch_id: BranchId,
738        merge_seq: SeqNo,
739    },
740    PhaseEntered {
741        phase: LoopPhase,
742    },
743    DeliberationProposed {
744        summary: String,
745        #[serde(default)]
746        proposed_tool: Option<String>,
747    },
748    RunStarted {
749        provider: String,
750        max_iterations: u32,
751    },
752    RunFinished {
753        reason: String,
754        total_iterations: u32,
755        #[serde(default)]
756        final_answer: Option<String>,
757        #[serde(default)]
758        usage: Option<TokenUsage>,
759    },
760    RunErrored {
761        error: String,
762    },
763    StepStarted {
764        index: u32,
765    },
766    StepFinished {
767        index: u32,
768        stop_reason: String,
769        directive_count: usize,
770    },
771    AssistantTextDelta {
772        delta: String,
773        #[serde(default)]
774        index: Option<u32>,
775    },
776    AssistantMessageCommitted {
777        role: String,
778        content: String,
779        #[serde(default)]
780        model: Option<String>,
781        #[serde(default)]
782        token_usage: Option<TokenUsage>,
783    },
784    TextDelta {
785        delta: String,
786        #[serde(default)]
787        index: Option<u32>,
788    },
789    Message {
790        role: String,
791        content: String,
792        #[serde(default)]
793        model: Option<String>,
794        #[serde(default)]
795        token_usage: Option<TokenUsage>,
796    },
797    ToolCallRequested {
798        call_id: String,
799        tool_name: String,
800        arguments: serde_json::Value,
801        #[serde(default)]
802        category: Option<String>,
803    },
804    ToolCallStarted {
805        tool_run_id: ToolRunId,
806        tool_name: String,
807    },
808    ToolCallCompleted {
809        tool_run_id: ToolRunId,
810        #[serde(default)]
811        call_id: Option<String>,
812        tool_name: String,
813        result: serde_json::Value,
814        duration_ms: u64,
815        status: SpanStatus,
816    },
817    ToolCallFailed {
818        call_id: String,
819        tool_name: String,
820        error: String,
821    },
822    FileWrite {
823        path: String,
824        blob_hash: BlobHash,
825        size_bytes: u64,
826        #[serde(default)]
827        content_type: Option<String>,
828    },
829    FileDelete {
830        path: String,
831    },
832    FileRename {
833        old_path: String,
834        new_path: String,
835    },
836    FileMutated {
837        path: String,
838        content_hash: String,
839    },
840    StatePatched {
841        #[serde(default)]
842        index: Option<u32>,
843        patch: serde_json::Value,
844        revision: u64,
845    },
846    StatePatchCommitted {
847        new_version: u64,
848        patch: StatePatch,
849    },
850    ContextCompacted {
851        dropped_count: usize,
852        tokens_before: usize,
853        tokens_after: usize,
854    },
855    PolicyEvaluated {
856        tool_name: String,
857        decision: PolicyDecisionKind,
858        #[serde(default)]
859        rule_id: Option<String>,
860        #[serde(default)]
861        explanation: Option<String>,
862    },
863    ApprovalRequested {
864        approval_id: ApprovalId,
865        call_id: String,
866        tool_name: String,
867        arguments: serde_json::Value,
868        risk: RiskLevel,
869    },
870    ApprovalResolved {
871        approval_id: ApprovalId,
872        decision: ApprovalDecision,
873        #[serde(default)]
874        reason: Option<String>,
875    },
876    SnapshotCreated {
877        snapshot_id: SnapshotId,
878        snapshot_type: SnapshotType,
879        covers_through_seq: SeqNo,
880        data_hash: BlobHash,
881    },
882    SandboxCreated {
883        sandbox_id: String,
884        tier: String,
885        config: serde_json::Value,
886    },
887    SandboxExecuted {
888        sandbox_id: String,
889        command: String,
890        exit_code: i32,
891        duration_ms: u64,
892    },
893    SandboxViolation {
894        sandbox_id: String,
895        violation_type: String,
896        details: String,
897    },
898    SandboxDestroyed {
899        sandbox_id: String,
900    },
901    ObservationAppended {
902        scope: MemoryScope,
903        observation_ref: BlobHash,
904        #[serde(default)]
905        source_run_id: Option<String>,
906    },
907    ReflectionCompacted {
908        scope: MemoryScope,
909        summary_ref: BlobHash,
910        covers_through_seq: SeqNo,
911    },
912    MemoryProposed {
913        scope: MemoryScope,
914        proposal_id: MemoryId,
915        entries_ref: BlobHash,
916        #[serde(default)]
917        source_run_id: Option<String>,
918    },
919    MemoryCommitted {
920        scope: MemoryScope,
921        memory_id: MemoryId,
922        committed_ref: BlobHash,
923        #[serde(default)]
924        supersedes: Option<MemoryId>,
925    },
926    MemoryTombstoned {
927        scope: MemoryScope,
928        memory_id: MemoryId,
929        reason: String,
930    },
931    Heartbeat {
932        summary: String,
933        #[serde(default)]
934        checkpoint_id: Option<CheckpointId>,
935    },
936    StateEstimated {
937        state: AgentStateVector,
938        mode: OperatingMode,
939    },
940    BudgetUpdated {
941        budget: BudgetState,
942        reason: String,
943    },
944    ModeChanged {
945        from: OperatingMode,
946        to: OperatingMode,
947        reason: String,
948    },
949    GatesUpdated {
950        gates: serde_json::Value,
951        reason: String,
952    },
953    CircuitBreakerTripped {
954        reason: String,
955        error_streak: u32,
956    },
957    CheckpointCreated {
958        checkpoint_id: CheckpointId,
959        event_sequence: u64,
960        state_hash: String,
961    },
962    CheckpointRestored {
963        checkpoint_id: CheckpointId,
964        restored_to_seq: u64,
965    },
966    VoiceSessionStarted {
967        voice_session_id: String,
968        adapter: String,
969        model: String,
970        sample_rate_hz: u32,
971        channels: u8,
972    },
973    VoiceInputChunk {
974        voice_session_id: String,
975        chunk_index: u64,
976        bytes: usize,
977        format: String,
978    },
979    VoiceOutputChunk {
980        voice_session_id: String,
981        chunk_index: u64,
982        bytes: usize,
983        format: String,
984    },
985    VoiceSessionStopped {
986        voice_session_id: String,
987        reason: String,
988    },
989    VoiceAdapterError {
990        voice_session_id: String,
991        message: String,
992    },
993    WorldModelObserved {
994        state_ref: BlobHash,
995        meta: serde_json::Value,
996    },
997    WorldModelRollout {
998        trajectory_ref: BlobHash,
999        #[serde(default)]
1000        score: Option<f32>,
1001    },
1002    IntentProposed {
1003        intent_id: String,
1004        kind: String,
1005        #[serde(default)]
1006        risk: Option<RiskLevel>,
1007    },
1008    IntentEvaluated {
1009        intent_id: String,
1010        allowed: bool,
1011        requires_approval: bool,
1012        #[serde(default)]
1013        reasons: Vec<String>,
1014    },
1015    IntentApproved {
1016        intent_id: String,
1017        #[serde(default)]
1018        actor: Option<String>,
1019    },
1020    IntentRejected {
1021        intent_id: String,
1022        #[serde(default)]
1023        reasons: Vec<String>,
1024    },
1025    HiveTaskCreated {
1026        hive_task_id: HiveTaskId,
1027        objective: String,
1028        agent_count: u32,
1029    },
1030    HiveArtifactShared {
1031        hive_task_id: HiveTaskId,
1032        source_session_id: SessionId,
1033        score: f32,
1034        mutation_summary: String,
1035    },
1036    HiveSelectionMade {
1037        hive_task_id: HiveTaskId,
1038        winning_session_id: SessionId,
1039        winning_score: f32,
1040        generation: u32,
1041    },
1042    HiveGenerationCompleted {
1043        hive_task_id: HiveTaskId,
1044        generation: u32,
1045        best_score: f32,
1046        agent_results: serde_json::Value,
1047    },
1048    HiveTaskCompleted {
1049        hive_task_id: HiveTaskId,
1050        total_generations: u32,
1051        total_trials: u32,
1052        final_score: f32,
1053    },
1054    Queued {
1055        queue_id: String,
1056        mode: SteeringMode,
1057        message: String,
1058    },
1059    Steered {
1060        queue_id: String,
1061        preempted_at: String,
1062    },
1063    QueueDrained {
1064        queue_id: String,
1065        processed: usize,
1066    },
1067    ErrorRaised {
1068        message: String,
1069    },
1070    Custom {
1071        event_type: String,
1072        data: serde_json::Value,
1073    },
1074}
1075
1076/// Forward-compatible deserializer: unknown variants become `Custom`.
1077impl<'de> Deserialize<'de> for EventKind {
1078    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1079    where
1080        D: serde::Deserializer<'de>,
1081    {
1082        let raw = serde_json::Value::deserialize(deserializer)?;
1083        match serde_json::from_value::<EventKindKnown>(raw.clone()) {
1084            Ok(known) => Ok(known.into()),
1085            Err(_) => {
1086                let event_type = raw
1087                    .get("type")
1088                    .and_then(|v| v.as_str())
1089                    .unwrap_or("Unknown")
1090                    .to_string();
1091                let mut data = raw;
1092                if let Some(obj) = data.as_object_mut() {
1093                    obj.remove("type");
1094                }
1095                Ok(EventKind::Custom { event_type, data })
1096            }
1097        }
1098    }
1099}
1100
1101/// Conversion from the known helper enum to the public EventKind.
1102/// This is mechanical — each variant maps 1:1.
1103impl From<EventKindKnown> for EventKind {
1104    #[allow(clippy::too_many_lines)]
1105    fn from(k: EventKindKnown) -> Self {
1106        match k {
1107            EventKindKnown::UserMessage { content } => Self::UserMessage { content },
1108            EventKindKnown::ExternalSignal { signal_type, data } => {
1109                Self::ExternalSignal { signal_type, data }
1110            }
1111            EventKindKnown::SessionCreated { name, config } => {
1112                Self::SessionCreated { name, config }
1113            }
1114            EventKindKnown::SessionResumed { from_snapshot } => {
1115                Self::SessionResumed { from_snapshot }
1116            }
1117            EventKindKnown::SessionClosed { reason } => Self::SessionClosed { reason },
1118            EventKindKnown::BranchCreated {
1119                new_branch_id,
1120                fork_point_seq,
1121                name,
1122            } => Self::BranchCreated {
1123                new_branch_id,
1124                fork_point_seq,
1125                name,
1126            },
1127            EventKindKnown::BranchMerged {
1128                source_branch_id,
1129                merge_seq,
1130            } => Self::BranchMerged {
1131                source_branch_id,
1132                merge_seq,
1133            },
1134            EventKindKnown::PhaseEntered { phase } => Self::PhaseEntered { phase },
1135            EventKindKnown::DeliberationProposed {
1136                summary,
1137                proposed_tool,
1138            } => Self::DeliberationProposed {
1139                summary,
1140                proposed_tool,
1141            },
1142            EventKindKnown::RunStarted {
1143                provider,
1144                max_iterations,
1145            } => Self::RunStarted {
1146                provider,
1147                max_iterations,
1148            },
1149            EventKindKnown::RunFinished {
1150                reason,
1151                total_iterations,
1152                final_answer,
1153                usage,
1154            } => Self::RunFinished {
1155                reason,
1156                total_iterations,
1157                final_answer,
1158                usage,
1159            },
1160            EventKindKnown::RunErrored { error } => Self::RunErrored { error },
1161            EventKindKnown::StepStarted { index } => Self::StepStarted { index },
1162            EventKindKnown::StepFinished {
1163                index,
1164                stop_reason,
1165                directive_count,
1166            } => Self::StepFinished {
1167                index,
1168                stop_reason,
1169                directive_count,
1170            },
1171            EventKindKnown::AssistantTextDelta { delta, index } => {
1172                Self::AssistantTextDelta { delta, index }
1173            }
1174            EventKindKnown::AssistantMessageCommitted {
1175                role,
1176                content,
1177                model,
1178                token_usage,
1179            } => Self::AssistantMessageCommitted {
1180                role,
1181                content,
1182                model,
1183                token_usage,
1184            },
1185            EventKindKnown::TextDelta { delta, index } => Self::TextDelta { delta, index },
1186            EventKindKnown::Message {
1187                role,
1188                content,
1189                model,
1190                token_usage,
1191            } => Self::Message {
1192                role,
1193                content,
1194                model,
1195                token_usage,
1196            },
1197            EventKindKnown::ToolCallRequested {
1198                call_id,
1199                tool_name,
1200                arguments,
1201                category,
1202            } => Self::ToolCallRequested {
1203                call_id,
1204                tool_name,
1205                arguments,
1206                category,
1207            },
1208            EventKindKnown::ToolCallStarted {
1209                tool_run_id,
1210                tool_name,
1211            } => Self::ToolCallStarted {
1212                tool_run_id,
1213                tool_name,
1214            },
1215            EventKindKnown::ToolCallCompleted {
1216                tool_run_id,
1217                call_id,
1218                tool_name,
1219                result,
1220                duration_ms,
1221                status,
1222            } => Self::ToolCallCompleted {
1223                tool_run_id,
1224                call_id,
1225                tool_name,
1226                result,
1227                duration_ms,
1228                status,
1229            },
1230            EventKindKnown::ToolCallFailed {
1231                call_id,
1232                tool_name,
1233                error,
1234            } => Self::ToolCallFailed {
1235                call_id,
1236                tool_name,
1237                error,
1238            },
1239            EventKindKnown::FileWrite {
1240                path,
1241                blob_hash,
1242                size_bytes,
1243                content_type,
1244            } => Self::FileWrite {
1245                path,
1246                blob_hash,
1247                size_bytes,
1248                content_type,
1249            },
1250            EventKindKnown::FileDelete { path } => Self::FileDelete { path },
1251            EventKindKnown::FileRename { old_path, new_path } => {
1252                Self::FileRename { old_path, new_path }
1253            }
1254            EventKindKnown::FileMutated { path, content_hash } => {
1255                Self::FileMutated { path, content_hash }
1256            }
1257            EventKindKnown::StatePatched {
1258                index,
1259                patch,
1260                revision,
1261            } => Self::StatePatched {
1262                index,
1263                patch,
1264                revision,
1265            },
1266            EventKindKnown::StatePatchCommitted { new_version, patch } => {
1267                Self::StatePatchCommitted { new_version, patch }
1268            }
1269            EventKindKnown::ContextCompacted {
1270                dropped_count,
1271                tokens_before,
1272                tokens_after,
1273            } => Self::ContextCompacted {
1274                dropped_count,
1275                tokens_before,
1276                tokens_after,
1277            },
1278            EventKindKnown::PolicyEvaluated {
1279                tool_name,
1280                decision,
1281                rule_id,
1282                explanation,
1283            } => Self::PolicyEvaluated {
1284                tool_name,
1285                decision,
1286                rule_id,
1287                explanation,
1288            },
1289            EventKindKnown::ApprovalRequested {
1290                approval_id,
1291                call_id,
1292                tool_name,
1293                arguments,
1294                risk,
1295            } => Self::ApprovalRequested {
1296                approval_id,
1297                call_id,
1298                tool_name,
1299                arguments,
1300                risk,
1301            },
1302            EventKindKnown::ApprovalResolved {
1303                approval_id,
1304                decision,
1305                reason,
1306            } => Self::ApprovalResolved {
1307                approval_id,
1308                decision,
1309                reason,
1310            },
1311            EventKindKnown::SnapshotCreated {
1312                snapshot_id,
1313                snapshot_type,
1314                covers_through_seq,
1315                data_hash,
1316            } => Self::SnapshotCreated {
1317                snapshot_id,
1318                snapshot_type,
1319                covers_through_seq,
1320                data_hash,
1321            },
1322            EventKindKnown::SandboxCreated {
1323                sandbox_id,
1324                tier,
1325                config,
1326            } => Self::SandboxCreated {
1327                sandbox_id,
1328                tier,
1329                config,
1330            },
1331            EventKindKnown::SandboxExecuted {
1332                sandbox_id,
1333                command,
1334                exit_code,
1335                duration_ms,
1336            } => Self::SandboxExecuted {
1337                sandbox_id,
1338                command,
1339                exit_code,
1340                duration_ms,
1341            },
1342            EventKindKnown::SandboxViolation {
1343                sandbox_id,
1344                violation_type,
1345                details,
1346            } => Self::SandboxViolation {
1347                sandbox_id,
1348                violation_type,
1349                details,
1350            },
1351            EventKindKnown::SandboxDestroyed { sandbox_id } => {
1352                Self::SandboxDestroyed { sandbox_id }
1353            }
1354            EventKindKnown::ObservationAppended {
1355                scope,
1356                observation_ref,
1357                source_run_id,
1358            } => Self::ObservationAppended {
1359                scope,
1360                observation_ref,
1361                source_run_id,
1362            },
1363            EventKindKnown::ReflectionCompacted {
1364                scope,
1365                summary_ref,
1366                covers_through_seq,
1367            } => Self::ReflectionCompacted {
1368                scope,
1369                summary_ref,
1370                covers_through_seq,
1371            },
1372            EventKindKnown::MemoryProposed {
1373                scope,
1374                proposal_id,
1375                entries_ref,
1376                source_run_id,
1377            } => Self::MemoryProposed {
1378                scope,
1379                proposal_id,
1380                entries_ref,
1381                source_run_id,
1382            },
1383            EventKindKnown::MemoryCommitted {
1384                scope,
1385                memory_id,
1386                committed_ref,
1387                supersedes,
1388            } => Self::MemoryCommitted {
1389                scope,
1390                memory_id,
1391                committed_ref,
1392                supersedes,
1393            },
1394            EventKindKnown::MemoryTombstoned {
1395                scope,
1396                memory_id,
1397                reason,
1398            } => Self::MemoryTombstoned {
1399                scope,
1400                memory_id,
1401                reason,
1402            },
1403            EventKindKnown::Heartbeat {
1404                summary,
1405                checkpoint_id,
1406            } => Self::Heartbeat {
1407                summary,
1408                checkpoint_id,
1409            },
1410            EventKindKnown::StateEstimated { state, mode } => Self::StateEstimated { state, mode },
1411            EventKindKnown::BudgetUpdated { budget, reason } => {
1412                Self::BudgetUpdated { budget, reason }
1413            }
1414            EventKindKnown::ModeChanged { from, to, reason } => {
1415                Self::ModeChanged { from, to, reason }
1416            }
1417            EventKindKnown::GatesUpdated { gates, reason } => Self::GatesUpdated { gates, reason },
1418            EventKindKnown::CircuitBreakerTripped {
1419                reason,
1420                error_streak,
1421            } => Self::CircuitBreakerTripped {
1422                reason,
1423                error_streak,
1424            },
1425            EventKindKnown::CheckpointCreated {
1426                checkpoint_id,
1427                event_sequence,
1428                state_hash,
1429            } => Self::CheckpointCreated {
1430                checkpoint_id,
1431                event_sequence,
1432                state_hash,
1433            },
1434            EventKindKnown::CheckpointRestored {
1435                checkpoint_id,
1436                restored_to_seq,
1437            } => Self::CheckpointRestored {
1438                checkpoint_id,
1439                restored_to_seq,
1440            },
1441            EventKindKnown::VoiceSessionStarted {
1442                voice_session_id,
1443                adapter,
1444                model,
1445                sample_rate_hz,
1446                channels,
1447            } => Self::VoiceSessionStarted {
1448                voice_session_id,
1449                adapter,
1450                model,
1451                sample_rate_hz,
1452                channels,
1453            },
1454            EventKindKnown::VoiceInputChunk {
1455                voice_session_id,
1456                chunk_index,
1457                bytes,
1458                format,
1459            } => Self::VoiceInputChunk {
1460                voice_session_id,
1461                chunk_index,
1462                bytes,
1463                format,
1464            },
1465            EventKindKnown::VoiceOutputChunk {
1466                voice_session_id,
1467                chunk_index,
1468                bytes,
1469                format,
1470            } => Self::VoiceOutputChunk {
1471                voice_session_id,
1472                chunk_index,
1473                bytes,
1474                format,
1475            },
1476            EventKindKnown::VoiceSessionStopped {
1477                voice_session_id,
1478                reason,
1479            } => Self::VoiceSessionStopped {
1480                voice_session_id,
1481                reason,
1482            },
1483            EventKindKnown::VoiceAdapterError {
1484                voice_session_id,
1485                message,
1486            } => Self::VoiceAdapterError {
1487                voice_session_id,
1488                message,
1489            },
1490            EventKindKnown::WorldModelObserved { state_ref, meta } => {
1491                Self::WorldModelObserved { state_ref, meta }
1492            }
1493            EventKindKnown::WorldModelRollout {
1494                trajectory_ref,
1495                score,
1496            } => Self::WorldModelRollout {
1497                trajectory_ref,
1498                score,
1499            },
1500            EventKindKnown::IntentProposed {
1501                intent_id,
1502                kind,
1503                risk,
1504            } => Self::IntentProposed {
1505                intent_id,
1506                kind,
1507                risk,
1508            },
1509            EventKindKnown::IntentEvaluated {
1510                intent_id,
1511                allowed,
1512                requires_approval,
1513                reasons,
1514            } => Self::IntentEvaluated {
1515                intent_id,
1516                allowed,
1517                requires_approval,
1518                reasons,
1519            },
1520            EventKindKnown::IntentApproved { intent_id, actor } => {
1521                Self::IntentApproved { intent_id, actor }
1522            }
1523            EventKindKnown::IntentRejected { intent_id, reasons } => {
1524                Self::IntentRejected { intent_id, reasons }
1525            }
1526            EventKindKnown::HiveTaskCreated {
1527                hive_task_id,
1528                objective,
1529                agent_count,
1530            } => Self::HiveTaskCreated {
1531                hive_task_id,
1532                objective,
1533                agent_count,
1534            },
1535            EventKindKnown::HiveArtifactShared {
1536                hive_task_id,
1537                source_session_id,
1538                score,
1539                mutation_summary,
1540            } => Self::HiveArtifactShared {
1541                hive_task_id,
1542                source_session_id,
1543                score,
1544                mutation_summary,
1545            },
1546            EventKindKnown::HiveSelectionMade {
1547                hive_task_id,
1548                winning_session_id,
1549                winning_score,
1550                generation,
1551            } => Self::HiveSelectionMade {
1552                hive_task_id,
1553                winning_session_id,
1554                winning_score,
1555                generation,
1556            },
1557            EventKindKnown::HiveGenerationCompleted {
1558                hive_task_id,
1559                generation,
1560                best_score,
1561                agent_results,
1562            } => Self::HiveGenerationCompleted {
1563                hive_task_id,
1564                generation,
1565                best_score,
1566                agent_results,
1567            },
1568            EventKindKnown::HiveTaskCompleted {
1569                hive_task_id,
1570                total_generations,
1571                total_trials,
1572                final_score,
1573            } => Self::HiveTaskCompleted {
1574                hive_task_id,
1575                total_generations,
1576                total_trials,
1577                final_score,
1578            },
1579            EventKindKnown::Queued {
1580                queue_id,
1581                mode,
1582                message,
1583            } => Self::Queued {
1584                queue_id,
1585                mode,
1586                message,
1587            },
1588            EventKindKnown::Steered {
1589                queue_id,
1590                preempted_at,
1591            } => Self::Steered {
1592                queue_id,
1593                preempted_at,
1594            },
1595            EventKindKnown::QueueDrained {
1596                queue_id,
1597                processed,
1598            } => Self::QueueDrained {
1599                queue_id,
1600                processed,
1601            },
1602            EventKindKnown::ErrorRaised { message } => Self::ErrorRaised { message },
1603            EventKindKnown::Custom { event_type, data } => Self::Custom { event_type, data },
1604        }
1605    }
1606}
1607
1608#[cfg(test)]
1609mod tests {
1610    use super::*;
1611
1612    fn make_envelope(kind: EventKind) -> EventEnvelope {
1613        EventEnvelope {
1614            event_id: EventId::from_string("EVT001"),
1615            session_id: SessionId::from_string("SESS001"),
1616            agent_id: AgentId::from_string("AGENT001"),
1617            branch_id: BranchId::from_string("main"),
1618            run_id: None,
1619            seq: 1,
1620            timestamp: 1_700_000_000_000_000,
1621            actor: EventActor::default(),
1622            schema: EventSchema::default(),
1623            parent_id: None,
1624            trace_id: None,
1625            span_id: None,
1626            digest: None,
1627            kind,
1628            metadata: HashMap::new(),
1629            schema_version: 1,
1630        }
1631    }
1632
1633    #[test]
1634    fn error_raised_roundtrip() {
1635        let kind = EventKind::ErrorRaised {
1636            message: "boom".into(),
1637        };
1638        let json = serde_json::to_string(&kind).unwrap();
1639        assert!(json.contains("\"type\":\"ErrorRaised\""));
1640        let back: EventKind = serde_json::from_str(&json).unwrap();
1641        assert!(matches!(back, EventKind::ErrorRaised { message } if message == "boom"));
1642    }
1643
1644    #[test]
1645    fn heartbeat_roundtrip() {
1646        let kind = EventKind::Heartbeat {
1647            summary: "alive".into(),
1648            checkpoint_id: None,
1649        };
1650        let json = serde_json::to_string(&kind).unwrap();
1651        let back: EventKind = serde_json::from_str(&json).unwrap();
1652        assert!(matches!(back, EventKind::Heartbeat { .. }));
1653    }
1654
1655    #[test]
1656    fn state_estimated_roundtrip() {
1657        let kind = EventKind::StateEstimated {
1658            state: AgentStateVector::default(),
1659            mode: OperatingMode::Execute,
1660        };
1661        let json = serde_json::to_string(&kind).unwrap();
1662        let back: EventKind = serde_json::from_str(&json).unwrap();
1663        assert!(matches!(back, EventKind::StateEstimated { .. }));
1664    }
1665
1666    #[test]
1667    fn unknown_variant_becomes_custom() {
1668        let json = r#"{"type":"FutureFeature","key":"value","num":42}"#;
1669        let kind: EventKind = serde_json::from_str(json).unwrap();
1670        if let EventKind::Custom { event_type, data } = kind {
1671            assert_eq!(event_type, "FutureFeature");
1672            assert_eq!(data["key"], "value");
1673            assert_eq!(data["num"], 42);
1674        } else {
1675            panic!("should be Custom");
1676        }
1677    }
1678
1679    #[test]
1680    fn full_envelope_roundtrip() {
1681        let envelope = make_envelope(EventKind::RunStarted {
1682            provider: "anthropic".into(),
1683            max_iterations: 10,
1684        });
1685        let json = serde_json::to_string(&envelope).unwrap();
1686        let back: EventEnvelope = serde_json::from_str(&json).unwrap();
1687        assert_eq!(back.seq, 1);
1688        assert_eq!(back.schema_version, 1);
1689        assert!(matches!(back.kind, EventKind::RunStarted { .. }));
1690    }
1691
1692    #[test]
1693    fn tool_call_lifecycle_roundtrip() {
1694        let requested = EventKind::ToolCallRequested {
1695            call_id: "c1".into(),
1696            tool_name: "read_file".into(),
1697            arguments: serde_json::json!({"path": "/etc/hosts"}),
1698            category: Some("fs".into()),
1699        };
1700        let json = serde_json::to_string(&requested).unwrap();
1701        let back: EventKind = serde_json::from_str(&json).unwrap();
1702        assert!(matches!(back, EventKind::ToolCallRequested { .. }));
1703    }
1704
1705    #[test]
1706    fn memory_events_roundtrip() {
1707        let proposed = EventKind::MemoryProposed {
1708            scope: MemoryScope::Agent,
1709            proposal_id: MemoryId::from_string("PROP001"),
1710            entries_ref: BlobHash::from_hex("abc"),
1711            source_run_id: None,
1712        };
1713        let json = serde_json::to_string(&proposed).unwrap();
1714        let back: EventKind = serde_json::from_str(&json).unwrap();
1715        assert!(matches!(back, EventKind::MemoryProposed { .. }));
1716    }
1717
1718    #[test]
1719    fn mode_changed_roundtrip() {
1720        let kind = EventKind::ModeChanged {
1721            from: OperatingMode::Execute,
1722            to: OperatingMode::Recover,
1723            reason: "error streak".into(),
1724        };
1725        let json = serde_json::to_string(&kind).unwrap();
1726        let back: EventKind = serde_json::from_str(&json).unwrap();
1727        assert!(matches!(back, EventKind::ModeChanged { .. }));
1728    }
1729
1730    #[test]
1731    fn schema_version_defaults_to_1() {
1732        let json = r#"{"event_id":"E1","session_id":"S1","branch_id":"main","seq":0,"timestamp":100,"kind":{"type":"ErrorRaised","message":"x"},"metadata":{}}"#;
1733        let envelope: EventEnvelope = serde_json::from_str(json).unwrap();
1734        assert_eq!(envelope.schema_version, 1);
1735    }
1736
1737    #[test]
1738    fn hive_task_created_roundtrip() {
1739        let kind = EventKind::HiveTaskCreated {
1740            hive_task_id: HiveTaskId::from_string("HIVE001"),
1741            objective: "optimize scoring".into(),
1742            agent_count: 3,
1743        };
1744        let json = serde_json::to_string(&kind).unwrap();
1745        assert!(json.contains("\"type\":\"HiveTaskCreated\""));
1746        let back: EventKind = serde_json::from_str(&json).unwrap();
1747        assert!(matches!(
1748            back,
1749            EventKind::HiveTaskCreated { agent_count: 3, .. }
1750        ));
1751    }
1752
1753    #[test]
1754    fn hive_artifact_shared_roundtrip() {
1755        let kind = EventKind::HiveArtifactShared {
1756            hive_task_id: HiveTaskId::from_string("HIVE001"),
1757            source_session_id: SessionId::from_string("SESS-A"),
1758            score: 0.87,
1759            mutation_summary: "rewrote parser".into(),
1760        };
1761        let json = serde_json::to_string(&kind).unwrap();
1762        let back: EventKind = serde_json::from_str(&json).unwrap();
1763        assert!(matches!(back, EventKind::HiveArtifactShared { .. }));
1764    }
1765
1766    #[test]
1767    fn hive_selection_made_roundtrip() {
1768        let kind = EventKind::HiveSelectionMade {
1769            hive_task_id: HiveTaskId::from_string("HIVE001"),
1770            winning_session_id: SessionId::from_string("SESS-B"),
1771            winning_score: 0.92,
1772            generation: 2,
1773        };
1774        let json = serde_json::to_string(&kind).unwrap();
1775        let back: EventKind = serde_json::from_str(&json).unwrap();
1776        assert!(matches!(
1777            back,
1778            EventKind::HiveSelectionMade { generation: 2, .. }
1779        ));
1780    }
1781
1782    #[test]
1783    fn hive_generation_completed_roundtrip() {
1784        let kind = EventKind::HiveGenerationCompleted {
1785            hive_task_id: HiveTaskId::from_string("HIVE001"),
1786            generation: 3,
1787            best_score: 0.95,
1788            agent_results: serde_json::json!({"agents": 3, "improved": true}),
1789        };
1790        let json = serde_json::to_string(&kind).unwrap();
1791        let back: EventKind = serde_json::from_str(&json).unwrap();
1792        assert!(matches!(
1793            back,
1794            EventKind::HiveGenerationCompleted { generation: 3, .. }
1795        ));
1796    }
1797
1798    #[test]
1799    fn hive_task_completed_roundtrip() {
1800        let kind = EventKind::HiveTaskCompleted {
1801            hive_task_id: HiveTaskId::from_string("HIVE001"),
1802            total_generations: 5,
1803            total_trials: 15,
1804            final_score: 0.98,
1805        };
1806        let json = serde_json::to_string(&kind).unwrap();
1807        let back: EventKind = serde_json::from_str(&json).unwrap();
1808        assert!(matches!(
1809            back,
1810            EventKind::HiveTaskCompleted {
1811                total_generations: 5,
1812                ..
1813            }
1814        ));
1815    }
1816
1817    #[test]
1818    fn hive_full_envelope_roundtrip() {
1819        let envelope = make_envelope(EventKind::HiveTaskCreated {
1820            hive_task_id: HiveTaskId::from_string("HIVE-ENV"),
1821            objective: "test envelope".into(),
1822            agent_count: 5,
1823        });
1824        let json = serde_json::to_string(&envelope).unwrap();
1825        let back: EventEnvelope = serde_json::from_str(&json).unwrap();
1826        assert!(matches!(
1827            back.kind,
1828            EventKind::HiveTaskCreated { agent_count: 5, .. }
1829        ));
1830    }
1831
1832    #[test]
1833    fn voice_events_roundtrip() {
1834        let kind = EventKind::VoiceSessionStarted {
1835            voice_session_id: "vs1".into(),
1836            adapter: "openai-realtime".into(),
1837            model: "gpt-4o-realtime".into(),
1838            sample_rate_hz: 24000,
1839            channels: 1,
1840        };
1841        let json = serde_json::to_string(&kind).unwrap();
1842        let back: EventKind = serde_json::from_str(&json).unwrap();
1843        assert!(matches!(back, EventKind::VoiceSessionStarted { .. }));
1844    }
1845}