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    // ── Error ──
591    ErrorRaised {
592        message: String,
593    },
594
595    // ── Forward-compatible catch-all ──
596    Custom {
597        event_type: String,
598        data: serde_json::Value,
599    },
600}
601
602// ─── Supporting types ──────────────────────────────────────────────
603
604/// Agent loop phase (from aiOS).
605#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
606#[serde(rename_all = "snake_case")]
607pub enum LoopPhase {
608    Perceive,
609    Deliberate,
610    Gate,
611    Execute,
612    Commit,
613    Reflect,
614    Sleep,
615}
616
617/// Token usage reported by LLM providers.
618#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
619pub struct TokenUsage {
620    #[serde(default)]
621    pub prompt_tokens: u32,
622    #[serde(default)]
623    pub completion_tokens: u32,
624    #[serde(default)]
625    pub total_tokens: u32,
626}
627
628/// Tool execution span status.
629#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
630#[serde(rename_all = "snake_case")]
631pub enum SpanStatus {
632    Ok,
633    Error,
634    Timeout,
635    Cancelled,
636}
637
638/// Risk level for policy evaluation. Includes Critical from Lago.
639#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
640#[serde(rename_all = "snake_case")]
641pub enum RiskLevel {
642    Low,
643    Medium,
644    High,
645    Critical,
646}
647
648/// Approval decision outcome.
649#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
650#[serde(rename_all = "snake_case")]
651pub enum ApprovalDecision {
652    Approved,
653    Denied,
654    Timeout,
655}
656
657/// Snapshot type.
658#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
659#[serde(rename_all = "snake_case")]
660pub enum SnapshotType {
661    Full,
662    Incremental,
663}
664
665/// Policy decision kind.
666#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
667#[serde(rename_all = "snake_case")]
668pub enum PolicyDecisionKind {
669    Allow,
670    Deny,
671    RequireApproval,
672}
673
674// ─── Forward-compatible deserializer ───────────────────────────────
675
676/// Internal helper enum for the forward-compatible deserializer.
677/// Mirrors EventKind exactly but derives Deserialize.
678#[derive(Deserialize)]
679#[serde(tag = "type")]
680enum EventKindKnown {
681    UserMessage {
682        content: String,
683    },
684    ExternalSignal {
685        signal_type: String,
686        data: serde_json::Value,
687    },
688    SessionCreated {
689        name: String,
690        config: serde_json::Value,
691    },
692    SessionResumed {
693        #[serde(default)]
694        from_snapshot: Option<SnapshotId>,
695    },
696    SessionClosed {
697        reason: String,
698    },
699    BranchCreated {
700        new_branch_id: BranchId,
701        fork_point_seq: SeqNo,
702        name: String,
703    },
704    BranchMerged {
705        source_branch_id: BranchId,
706        merge_seq: SeqNo,
707    },
708    PhaseEntered {
709        phase: LoopPhase,
710    },
711    DeliberationProposed {
712        summary: String,
713        #[serde(default)]
714        proposed_tool: Option<String>,
715    },
716    RunStarted {
717        provider: String,
718        max_iterations: u32,
719    },
720    RunFinished {
721        reason: String,
722        total_iterations: u32,
723        #[serde(default)]
724        final_answer: Option<String>,
725        #[serde(default)]
726        usage: Option<TokenUsage>,
727    },
728    RunErrored {
729        error: String,
730    },
731    StepStarted {
732        index: u32,
733    },
734    StepFinished {
735        index: u32,
736        stop_reason: String,
737        directive_count: usize,
738    },
739    AssistantTextDelta {
740        delta: String,
741        #[serde(default)]
742        index: Option<u32>,
743    },
744    AssistantMessageCommitted {
745        role: String,
746        content: String,
747        #[serde(default)]
748        model: Option<String>,
749        #[serde(default)]
750        token_usage: Option<TokenUsage>,
751    },
752    TextDelta {
753        delta: String,
754        #[serde(default)]
755        index: Option<u32>,
756    },
757    Message {
758        role: String,
759        content: String,
760        #[serde(default)]
761        model: Option<String>,
762        #[serde(default)]
763        token_usage: Option<TokenUsage>,
764    },
765    ToolCallRequested {
766        call_id: String,
767        tool_name: String,
768        arguments: serde_json::Value,
769        #[serde(default)]
770        category: Option<String>,
771    },
772    ToolCallStarted {
773        tool_run_id: ToolRunId,
774        tool_name: String,
775    },
776    ToolCallCompleted {
777        tool_run_id: ToolRunId,
778        #[serde(default)]
779        call_id: Option<String>,
780        tool_name: String,
781        result: serde_json::Value,
782        duration_ms: u64,
783        status: SpanStatus,
784    },
785    ToolCallFailed {
786        call_id: String,
787        tool_name: String,
788        error: String,
789    },
790    FileWrite {
791        path: String,
792        blob_hash: BlobHash,
793        size_bytes: u64,
794        #[serde(default)]
795        content_type: Option<String>,
796    },
797    FileDelete {
798        path: String,
799    },
800    FileRename {
801        old_path: String,
802        new_path: String,
803    },
804    FileMutated {
805        path: String,
806        content_hash: String,
807    },
808    StatePatched {
809        #[serde(default)]
810        index: Option<u32>,
811        patch: serde_json::Value,
812        revision: u64,
813    },
814    StatePatchCommitted {
815        new_version: u64,
816        patch: StatePatch,
817    },
818    ContextCompacted {
819        dropped_count: usize,
820        tokens_before: usize,
821        tokens_after: usize,
822    },
823    PolicyEvaluated {
824        tool_name: String,
825        decision: PolicyDecisionKind,
826        #[serde(default)]
827        rule_id: Option<String>,
828        #[serde(default)]
829        explanation: Option<String>,
830    },
831    ApprovalRequested {
832        approval_id: ApprovalId,
833        call_id: String,
834        tool_name: String,
835        arguments: serde_json::Value,
836        risk: RiskLevel,
837    },
838    ApprovalResolved {
839        approval_id: ApprovalId,
840        decision: ApprovalDecision,
841        #[serde(default)]
842        reason: Option<String>,
843    },
844    SnapshotCreated {
845        snapshot_id: SnapshotId,
846        snapshot_type: SnapshotType,
847        covers_through_seq: SeqNo,
848        data_hash: BlobHash,
849    },
850    SandboxCreated {
851        sandbox_id: String,
852        tier: String,
853        config: serde_json::Value,
854    },
855    SandboxExecuted {
856        sandbox_id: String,
857        command: String,
858        exit_code: i32,
859        duration_ms: u64,
860    },
861    SandboxViolation {
862        sandbox_id: String,
863        violation_type: String,
864        details: String,
865    },
866    SandboxDestroyed {
867        sandbox_id: String,
868    },
869    ObservationAppended {
870        scope: MemoryScope,
871        observation_ref: BlobHash,
872        #[serde(default)]
873        source_run_id: Option<String>,
874    },
875    ReflectionCompacted {
876        scope: MemoryScope,
877        summary_ref: BlobHash,
878        covers_through_seq: SeqNo,
879    },
880    MemoryProposed {
881        scope: MemoryScope,
882        proposal_id: MemoryId,
883        entries_ref: BlobHash,
884        #[serde(default)]
885        source_run_id: Option<String>,
886    },
887    MemoryCommitted {
888        scope: MemoryScope,
889        memory_id: MemoryId,
890        committed_ref: BlobHash,
891        #[serde(default)]
892        supersedes: Option<MemoryId>,
893    },
894    MemoryTombstoned {
895        scope: MemoryScope,
896        memory_id: MemoryId,
897        reason: String,
898    },
899    Heartbeat {
900        summary: String,
901        #[serde(default)]
902        checkpoint_id: Option<CheckpointId>,
903    },
904    StateEstimated {
905        state: AgentStateVector,
906        mode: OperatingMode,
907    },
908    BudgetUpdated {
909        budget: BudgetState,
910        reason: String,
911    },
912    ModeChanged {
913        from: OperatingMode,
914        to: OperatingMode,
915        reason: String,
916    },
917    GatesUpdated {
918        gates: serde_json::Value,
919        reason: String,
920    },
921    CircuitBreakerTripped {
922        reason: String,
923        error_streak: u32,
924    },
925    CheckpointCreated {
926        checkpoint_id: CheckpointId,
927        event_sequence: u64,
928        state_hash: String,
929    },
930    CheckpointRestored {
931        checkpoint_id: CheckpointId,
932        restored_to_seq: u64,
933    },
934    VoiceSessionStarted {
935        voice_session_id: String,
936        adapter: String,
937        model: String,
938        sample_rate_hz: u32,
939        channels: u8,
940    },
941    VoiceInputChunk {
942        voice_session_id: String,
943        chunk_index: u64,
944        bytes: usize,
945        format: String,
946    },
947    VoiceOutputChunk {
948        voice_session_id: String,
949        chunk_index: u64,
950        bytes: usize,
951        format: String,
952    },
953    VoiceSessionStopped {
954        voice_session_id: String,
955        reason: String,
956    },
957    VoiceAdapterError {
958        voice_session_id: String,
959        message: String,
960    },
961    WorldModelObserved {
962        state_ref: BlobHash,
963        meta: serde_json::Value,
964    },
965    WorldModelRollout {
966        trajectory_ref: BlobHash,
967        #[serde(default)]
968        score: Option<f32>,
969    },
970    IntentProposed {
971        intent_id: String,
972        kind: String,
973        #[serde(default)]
974        risk: Option<RiskLevel>,
975    },
976    IntentEvaluated {
977        intent_id: String,
978        allowed: bool,
979        requires_approval: bool,
980        #[serde(default)]
981        reasons: Vec<String>,
982    },
983    IntentApproved {
984        intent_id: String,
985        #[serde(default)]
986        actor: Option<String>,
987    },
988    IntentRejected {
989        intent_id: String,
990        #[serde(default)]
991        reasons: Vec<String>,
992    },
993    HiveTaskCreated {
994        hive_task_id: HiveTaskId,
995        objective: String,
996        agent_count: u32,
997    },
998    HiveArtifactShared {
999        hive_task_id: HiveTaskId,
1000        source_session_id: SessionId,
1001        score: f32,
1002        mutation_summary: String,
1003    },
1004    HiveSelectionMade {
1005        hive_task_id: HiveTaskId,
1006        winning_session_id: SessionId,
1007        winning_score: f32,
1008        generation: u32,
1009    },
1010    HiveGenerationCompleted {
1011        hive_task_id: HiveTaskId,
1012        generation: u32,
1013        best_score: f32,
1014        agent_results: serde_json::Value,
1015    },
1016    HiveTaskCompleted {
1017        hive_task_id: HiveTaskId,
1018        total_generations: u32,
1019        total_trials: u32,
1020        final_score: f32,
1021    },
1022    ErrorRaised {
1023        message: String,
1024    },
1025    Custom {
1026        event_type: String,
1027        data: serde_json::Value,
1028    },
1029}
1030
1031/// Forward-compatible deserializer: unknown variants become `Custom`.
1032impl<'de> Deserialize<'de> for EventKind {
1033    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1034    where
1035        D: serde::Deserializer<'de>,
1036    {
1037        let raw = serde_json::Value::deserialize(deserializer)?;
1038        match serde_json::from_value::<EventKindKnown>(raw.clone()) {
1039            Ok(known) => Ok(known.into()),
1040            Err(_) => {
1041                let event_type = raw
1042                    .get("type")
1043                    .and_then(|v| v.as_str())
1044                    .unwrap_or("Unknown")
1045                    .to_string();
1046                let mut data = raw;
1047                if let Some(obj) = data.as_object_mut() {
1048                    obj.remove("type");
1049                }
1050                Ok(EventKind::Custom { event_type, data })
1051            }
1052        }
1053    }
1054}
1055
1056/// Conversion from the known helper enum to the public EventKind.
1057/// This is mechanical — each variant maps 1:1.
1058impl From<EventKindKnown> for EventKind {
1059    #[allow(clippy::too_many_lines)]
1060    fn from(k: EventKindKnown) -> Self {
1061        match k {
1062            EventKindKnown::UserMessage { content } => Self::UserMessage { content },
1063            EventKindKnown::ExternalSignal { signal_type, data } => {
1064                Self::ExternalSignal { signal_type, data }
1065            }
1066            EventKindKnown::SessionCreated { name, config } => {
1067                Self::SessionCreated { name, config }
1068            }
1069            EventKindKnown::SessionResumed { from_snapshot } => {
1070                Self::SessionResumed { from_snapshot }
1071            }
1072            EventKindKnown::SessionClosed { reason } => Self::SessionClosed { reason },
1073            EventKindKnown::BranchCreated {
1074                new_branch_id,
1075                fork_point_seq,
1076                name,
1077            } => Self::BranchCreated {
1078                new_branch_id,
1079                fork_point_seq,
1080                name,
1081            },
1082            EventKindKnown::BranchMerged {
1083                source_branch_id,
1084                merge_seq,
1085            } => Self::BranchMerged {
1086                source_branch_id,
1087                merge_seq,
1088            },
1089            EventKindKnown::PhaseEntered { phase } => Self::PhaseEntered { phase },
1090            EventKindKnown::DeliberationProposed {
1091                summary,
1092                proposed_tool,
1093            } => Self::DeliberationProposed {
1094                summary,
1095                proposed_tool,
1096            },
1097            EventKindKnown::RunStarted {
1098                provider,
1099                max_iterations,
1100            } => Self::RunStarted {
1101                provider,
1102                max_iterations,
1103            },
1104            EventKindKnown::RunFinished {
1105                reason,
1106                total_iterations,
1107                final_answer,
1108                usage,
1109            } => Self::RunFinished {
1110                reason,
1111                total_iterations,
1112                final_answer,
1113                usage,
1114            },
1115            EventKindKnown::RunErrored { error } => Self::RunErrored { error },
1116            EventKindKnown::StepStarted { index } => Self::StepStarted { index },
1117            EventKindKnown::StepFinished {
1118                index,
1119                stop_reason,
1120                directive_count,
1121            } => Self::StepFinished {
1122                index,
1123                stop_reason,
1124                directive_count,
1125            },
1126            EventKindKnown::AssistantTextDelta { delta, index } => {
1127                Self::AssistantTextDelta { delta, index }
1128            }
1129            EventKindKnown::AssistantMessageCommitted {
1130                role,
1131                content,
1132                model,
1133                token_usage,
1134            } => Self::AssistantMessageCommitted {
1135                role,
1136                content,
1137                model,
1138                token_usage,
1139            },
1140            EventKindKnown::TextDelta { delta, index } => Self::TextDelta { delta, index },
1141            EventKindKnown::Message {
1142                role,
1143                content,
1144                model,
1145                token_usage,
1146            } => Self::Message {
1147                role,
1148                content,
1149                model,
1150                token_usage,
1151            },
1152            EventKindKnown::ToolCallRequested {
1153                call_id,
1154                tool_name,
1155                arguments,
1156                category,
1157            } => Self::ToolCallRequested {
1158                call_id,
1159                tool_name,
1160                arguments,
1161                category,
1162            },
1163            EventKindKnown::ToolCallStarted {
1164                tool_run_id,
1165                tool_name,
1166            } => Self::ToolCallStarted {
1167                tool_run_id,
1168                tool_name,
1169            },
1170            EventKindKnown::ToolCallCompleted {
1171                tool_run_id,
1172                call_id,
1173                tool_name,
1174                result,
1175                duration_ms,
1176                status,
1177            } => Self::ToolCallCompleted {
1178                tool_run_id,
1179                call_id,
1180                tool_name,
1181                result,
1182                duration_ms,
1183                status,
1184            },
1185            EventKindKnown::ToolCallFailed {
1186                call_id,
1187                tool_name,
1188                error,
1189            } => Self::ToolCallFailed {
1190                call_id,
1191                tool_name,
1192                error,
1193            },
1194            EventKindKnown::FileWrite {
1195                path,
1196                blob_hash,
1197                size_bytes,
1198                content_type,
1199            } => Self::FileWrite {
1200                path,
1201                blob_hash,
1202                size_bytes,
1203                content_type,
1204            },
1205            EventKindKnown::FileDelete { path } => Self::FileDelete { path },
1206            EventKindKnown::FileRename { old_path, new_path } => {
1207                Self::FileRename { old_path, new_path }
1208            }
1209            EventKindKnown::FileMutated { path, content_hash } => {
1210                Self::FileMutated { path, content_hash }
1211            }
1212            EventKindKnown::StatePatched {
1213                index,
1214                patch,
1215                revision,
1216            } => Self::StatePatched {
1217                index,
1218                patch,
1219                revision,
1220            },
1221            EventKindKnown::StatePatchCommitted { new_version, patch } => {
1222                Self::StatePatchCommitted { new_version, patch }
1223            }
1224            EventKindKnown::ContextCompacted {
1225                dropped_count,
1226                tokens_before,
1227                tokens_after,
1228            } => Self::ContextCompacted {
1229                dropped_count,
1230                tokens_before,
1231                tokens_after,
1232            },
1233            EventKindKnown::PolicyEvaluated {
1234                tool_name,
1235                decision,
1236                rule_id,
1237                explanation,
1238            } => Self::PolicyEvaluated {
1239                tool_name,
1240                decision,
1241                rule_id,
1242                explanation,
1243            },
1244            EventKindKnown::ApprovalRequested {
1245                approval_id,
1246                call_id,
1247                tool_name,
1248                arguments,
1249                risk,
1250            } => Self::ApprovalRequested {
1251                approval_id,
1252                call_id,
1253                tool_name,
1254                arguments,
1255                risk,
1256            },
1257            EventKindKnown::ApprovalResolved {
1258                approval_id,
1259                decision,
1260                reason,
1261            } => Self::ApprovalResolved {
1262                approval_id,
1263                decision,
1264                reason,
1265            },
1266            EventKindKnown::SnapshotCreated {
1267                snapshot_id,
1268                snapshot_type,
1269                covers_through_seq,
1270                data_hash,
1271            } => Self::SnapshotCreated {
1272                snapshot_id,
1273                snapshot_type,
1274                covers_through_seq,
1275                data_hash,
1276            },
1277            EventKindKnown::SandboxCreated {
1278                sandbox_id,
1279                tier,
1280                config,
1281            } => Self::SandboxCreated {
1282                sandbox_id,
1283                tier,
1284                config,
1285            },
1286            EventKindKnown::SandboxExecuted {
1287                sandbox_id,
1288                command,
1289                exit_code,
1290                duration_ms,
1291            } => Self::SandboxExecuted {
1292                sandbox_id,
1293                command,
1294                exit_code,
1295                duration_ms,
1296            },
1297            EventKindKnown::SandboxViolation {
1298                sandbox_id,
1299                violation_type,
1300                details,
1301            } => Self::SandboxViolation {
1302                sandbox_id,
1303                violation_type,
1304                details,
1305            },
1306            EventKindKnown::SandboxDestroyed { sandbox_id } => {
1307                Self::SandboxDestroyed { sandbox_id }
1308            }
1309            EventKindKnown::ObservationAppended {
1310                scope,
1311                observation_ref,
1312                source_run_id,
1313            } => Self::ObservationAppended {
1314                scope,
1315                observation_ref,
1316                source_run_id,
1317            },
1318            EventKindKnown::ReflectionCompacted {
1319                scope,
1320                summary_ref,
1321                covers_through_seq,
1322            } => Self::ReflectionCompacted {
1323                scope,
1324                summary_ref,
1325                covers_through_seq,
1326            },
1327            EventKindKnown::MemoryProposed {
1328                scope,
1329                proposal_id,
1330                entries_ref,
1331                source_run_id,
1332            } => Self::MemoryProposed {
1333                scope,
1334                proposal_id,
1335                entries_ref,
1336                source_run_id,
1337            },
1338            EventKindKnown::MemoryCommitted {
1339                scope,
1340                memory_id,
1341                committed_ref,
1342                supersedes,
1343            } => Self::MemoryCommitted {
1344                scope,
1345                memory_id,
1346                committed_ref,
1347                supersedes,
1348            },
1349            EventKindKnown::MemoryTombstoned {
1350                scope,
1351                memory_id,
1352                reason,
1353            } => Self::MemoryTombstoned {
1354                scope,
1355                memory_id,
1356                reason,
1357            },
1358            EventKindKnown::Heartbeat {
1359                summary,
1360                checkpoint_id,
1361            } => Self::Heartbeat {
1362                summary,
1363                checkpoint_id,
1364            },
1365            EventKindKnown::StateEstimated { state, mode } => Self::StateEstimated { state, mode },
1366            EventKindKnown::BudgetUpdated { budget, reason } => {
1367                Self::BudgetUpdated { budget, reason }
1368            }
1369            EventKindKnown::ModeChanged { from, to, reason } => {
1370                Self::ModeChanged { from, to, reason }
1371            }
1372            EventKindKnown::GatesUpdated { gates, reason } => Self::GatesUpdated { gates, reason },
1373            EventKindKnown::CircuitBreakerTripped {
1374                reason,
1375                error_streak,
1376            } => Self::CircuitBreakerTripped {
1377                reason,
1378                error_streak,
1379            },
1380            EventKindKnown::CheckpointCreated {
1381                checkpoint_id,
1382                event_sequence,
1383                state_hash,
1384            } => Self::CheckpointCreated {
1385                checkpoint_id,
1386                event_sequence,
1387                state_hash,
1388            },
1389            EventKindKnown::CheckpointRestored {
1390                checkpoint_id,
1391                restored_to_seq,
1392            } => Self::CheckpointRestored {
1393                checkpoint_id,
1394                restored_to_seq,
1395            },
1396            EventKindKnown::VoiceSessionStarted {
1397                voice_session_id,
1398                adapter,
1399                model,
1400                sample_rate_hz,
1401                channels,
1402            } => Self::VoiceSessionStarted {
1403                voice_session_id,
1404                adapter,
1405                model,
1406                sample_rate_hz,
1407                channels,
1408            },
1409            EventKindKnown::VoiceInputChunk {
1410                voice_session_id,
1411                chunk_index,
1412                bytes,
1413                format,
1414            } => Self::VoiceInputChunk {
1415                voice_session_id,
1416                chunk_index,
1417                bytes,
1418                format,
1419            },
1420            EventKindKnown::VoiceOutputChunk {
1421                voice_session_id,
1422                chunk_index,
1423                bytes,
1424                format,
1425            } => Self::VoiceOutputChunk {
1426                voice_session_id,
1427                chunk_index,
1428                bytes,
1429                format,
1430            },
1431            EventKindKnown::VoiceSessionStopped {
1432                voice_session_id,
1433                reason,
1434            } => Self::VoiceSessionStopped {
1435                voice_session_id,
1436                reason,
1437            },
1438            EventKindKnown::VoiceAdapterError {
1439                voice_session_id,
1440                message,
1441            } => Self::VoiceAdapterError {
1442                voice_session_id,
1443                message,
1444            },
1445            EventKindKnown::WorldModelObserved { state_ref, meta } => {
1446                Self::WorldModelObserved { state_ref, meta }
1447            }
1448            EventKindKnown::WorldModelRollout {
1449                trajectory_ref,
1450                score,
1451            } => Self::WorldModelRollout {
1452                trajectory_ref,
1453                score,
1454            },
1455            EventKindKnown::IntentProposed {
1456                intent_id,
1457                kind,
1458                risk,
1459            } => Self::IntentProposed {
1460                intent_id,
1461                kind,
1462                risk,
1463            },
1464            EventKindKnown::IntentEvaluated {
1465                intent_id,
1466                allowed,
1467                requires_approval,
1468                reasons,
1469            } => Self::IntentEvaluated {
1470                intent_id,
1471                allowed,
1472                requires_approval,
1473                reasons,
1474            },
1475            EventKindKnown::IntentApproved { intent_id, actor } => {
1476                Self::IntentApproved { intent_id, actor }
1477            }
1478            EventKindKnown::IntentRejected { intent_id, reasons } => {
1479                Self::IntentRejected { intent_id, reasons }
1480            }
1481            EventKindKnown::HiveTaskCreated {
1482                hive_task_id,
1483                objective,
1484                agent_count,
1485            } => Self::HiveTaskCreated {
1486                hive_task_id,
1487                objective,
1488                agent_count,
1489            },
1490            EventKindKnown::HiveArtifactShared {
1491                hive_task_id,
1492                source_session_id,
1493                score,
1494                mutation_summary,
1495            } => Self::HiveArtifactShared {
1496                hive_task_id,
1497                source_session_id,
1498                score,
1499                mutation_summary,
1500            },
1501            EventKindKnown::HiveSelectionMade {
1502                hive_task_id,
1503                winning_session_id,
1504                winning_score,
1505                generation,
1506            } => Self::HiveSelectionMade {
1507                hive_task_id,
1508                winning_session_id,
1509                winning_score,
1510                generation,
1511            },
1512            EventKindKnown::HiveGenerationCompleted {
1513                hive_task_id,
1514                generation,
1515                best_score,
1516                agent_results,
1517            } => Self::HiveGenerationCompleted {
1518                hive_task_id,
1519                generation,
1520                best_score,
1521                agent_results,
1522            },
1523            EventKindKnown::HiveTaskCompleted {
1524                hive_task_id,
1525                total_generations,
1526                total_trials,
1527                final_score,
1528            } => Self::HiveTaskCompleted {
1529                hive_task_id,
1530                total_generations,
1531                total_trials,
1532                final_score,
1533            },
1534            EventKindKnown::ErrorRaised { message } => Self::ErrorRaised { message },
1535            EventKindKnown::Custom { event_type, data } => Self::Custom { event_type, data },
1536        }
1537    }
1538}
1539
1540#[cfg(test)]
1541mod tests {
1542    use super::*;
1543
1544    fn make_envelope(kind: EventKind) -> EventEnvelope {
1545        EventEnvelope {
1546            event_id: EventId::from_string("EVT001"),
1547            session_id: SessionId::from_string("SESS001"),
1548            agent_id: AgentId::from_string("AGENT001"),
1549            branch_id: BranchId::from_string("main"),
1550            run_id: None,
1551            seq: 1,
1552            timestamp: 1_700_000_000_000_000,
1553            actor: EventActor::default(),
1554            schema: EventSchema::default(),
1555            parent_id: None,
1556            trace_id: None,
1557            span_id: None,
1558            digest: None,
1559            kind,
1560            metadata: HashMap::new(),
1561            schema_version: 1,
1562        }
1563    }
1564
1565    #[test]
1566    fn error_raised_roundtrip() {
1567        let kind = EventKind::ErrorRaised {
1568            message: "boom".into(),
1569        };
1570        let json = serde_json::to_string(&kind).unwrap();
1571        assert!(json.contains("\"type\":\"ErrorRaised\""));
1572        let back: EventKind = serde_json::from_str(&json).unwrap();
1573        assert!(matches!(back, EventKind::ErrorRaised { message } if message == "boom"));
1574    }
1575
1576    #[test]
1577    fn heartbeat_roundtrip() {
1578        let kind = EventKind::Heartbeat {
1579            summary: "alive".into(),
1580            checkpoint_id: None,
1581        };
1582        let json = serde_json::to_string(&kind).unwrap();
1583        let back: EventKind = serde_json::from_str(&json).unwrap();
1584        assert!(matches!(back, EventKind::Heartbeat { .. }));
1585    }
1586
1587    #[test]
1588    fn state_estimated_roundtrip() {
1589        let kind = EventKind::StateEstimated {
1590            state: AgentStateVector::default(),
1591            mode: OperatingMode::Execute,
1592        };
1593        let json = serde_json::to_string(&kind).unwrap();
1594        let back: EventKind = serde_json::from_str(&json).unwrap();
1595        assert!(matches!(back, EventKind::StateEstimated { .. }));
1596    }
1597
1598    #[test]
1599    fn unknown_variant_becomes_custom() {
1600        let json = r#"{"type":"FutureFeature","key":"value","num":42}"#;
1601        let kind: EventKind = serde_json::from_str(json).unwrap();
1602        if let EventKind::Custom { event_type, data } = kind {
1603            assert_eq!(event_type, "FutureFeature");
1604            assert_eq!(data["key"], "value");
1605            assert_eq!(data["num"], 42);
1606        } else {
1607            panic!("should be Custom");
1608        }
1609    }
1610
1611    #[test]
1612    fn full_envelope_roundtrip() {
1613        let envelope = make_envelope(EventKind::RunStarted {
1614            provider: "anthropic".into(),
1615            max_iterations: 10,
1616        });
1617        let json = serde_json::to_string(&envelope).unwrap();
1618        let back: EventEnvelope = serde_json::from_str(&json).unwrap();
1619        assert_eq!(back.seq, 1);
1620        assert_eq!(back.schema_version, 1);
1621        assert!(matches!(back.kind, EventKind::RunStarted { .. }));
1622    }
1623
1624    #[test]
1625    fn tool_call_lifecycle_roundtrip() {
1626        let requested = EventKind::ToolCallRequested {
1627            call_id: "c1".into(),
1628            tool_name: "read_file".into(),
1629            arguments: serde_json::json!({"path": "/etc/hosts"}),
1630            category: Some("fs".into()),
1631        };
1632        let json = serde_json::to_string(&requested).unwrap();
1633        let back: EventKind = serde_json::from_str(&json).unwrap();
1634        assert!(matches!(back, EventKind::ToolCallRequested { .. }));
1635    }
1636
1637    #[test]
1638    fn memory_events_roundtrip() {
1639        let proposed = EventKind::MemoryProposed {
1640            scope: MemoryScope::Agent,
1641            proposal_id: MemoryId::from_string("PROP001"),
1642            entries_ref: BlobHash::from_hex("abc"),
1643            source_run_id: None,
1644        };
1645        let json = serde_json::to_string(&proposed).unwrap();
1646        let back: EventKind = serde_json::from_str(&json).unwrap();
1647        assert!(matches!(back, EventKind::MemoryProposed { .. }));
1648    }
1649
1650    #[test]
1651    fn mode_changed_roundtrip() {
1652        let kind = EventKind::ModeChanged {
1653            from: OperatingMode::Execute,
1654            to: OperatingMode::Recover,
1655            reason: "error streak".into(),
1656        };
1657        let json = serde_json::to_string(&kind).unwrap();
1658        let back: EventKind = serde_json::from_str(&json).unwrap();
1659        assert!(matches!(back, EventKind::ModeChanged { .. }));
1660    }
1661
1662    #[test]
1663    fn schema_version_defaults_to_1() {
1664        let json = r#"{"event_id":"E1","session_id":"S1","branch_id":"main","seq":0,"timestamp":100,"kind":{"type":"ErrorRaised","message":"x"},"metadata":{}}"#;
1665        let envelope: EventEnvelope = serde_json::from_str(json).unwrap();
1666        assert_eq!(envelope.schema_version, 1);
1667    }
1668
1669    #[test]
1670    fn hive_task_created_roundtrip() {
1671        let kind = EventKind::HiveTaskCreated {
1672            hive_task_id: HiveTaskId::from_string("HIVE001"),
1673            objective: "optimize scoring".into(),
1674            agent_count: 3,
1675        };
1676        let json = serde_json::to_string(&kind).unwrap();
1677        assert!(json.contains("\"type\":\"HiveTaskCreated\""));
1678        let back: EventKind = serde_json::from_str(&json).unwrap();
1679        assert!(matches!(
1680            back,
1681            EventKind::HiveTaskCreated { agent_count: 3, .. }
1682        ));
1683    }
1684
1685    #[test]
1686    fn hive_artifact_shared_roundtrip() {
1687        let kind = EventKind::HiveArtifactShared {
1688            hive_task_id: HiveTaskId::from_string("HIVE001"),
1689            source_session_id: SessionId::from_string("SESS-A"),
1690            score: 0.87,
1691            mutation_summary: "rewrote parser".into(),
1692        };
1693        let json = serde_json::to_string(&kind).unwrap();
1694        let back: EventKind = serde_json::from_str(&json).unwrap();
1695        assert!(matches!(back, EventKind::HiveArtifactShared { .. }));
1696    }
1697
1698    #[test]
1699    fn hive_selection_made_roundtrip() {
1700        let kind = EventKind::HiveSelectionMade {
1701            hive_task_id: HiveTaskId::from_string("HIVE001"),
1702            winning_session_id: SessionId::from_string("SESS-B"),
1703            winning_score: 0.92,
1704            generation: 2,
1705        };
1706        let json = serde_json::to_string(&kind).unwrap();
1707        let back: EventKind = serde_json::from_str(&json).unwrap();
1708        assert!(matches!(
1709            back,
1710            EventKind::HiveSelectionMade { generation: 2, .. }
1711        ));
1712    }
1713
1714    #[test]
1715    fn hive_generation_completed_roundtrip() {
1716        let kind = EventKind::HiveGenerationCompleted {
1717            hive_task_id: HiveTaskId::from_string("HIVE001"),
1718            generation: 3,
1719            best_score: 0.95,
1720            agent_results: serde_json::json!({"agents": 3, "improved": true}),
1721        };
1722        let json = serde_json::to_string(&kind).unwrap();
1723        let back: EventKind = serde_json::from_str(&json).unwrap();
1724        assert!(matches!(
1725            back,
1726            EventKind::HiveGenerationCompleted { generation: 3, .. }
1727        ));
1728    }
1729
1730    #[test]
1731    fn hive_task_completed_roundtrip() {
1732        let kind = EventKind::HiveTaskCompleted {
1733            hive_task_id: HiveTaskId::from_string("HIVE001"),
1734            total_generations: 5,
1735            total_trials: 15,
1736            final_score: 0.98,
1737        };
1738        let json = serde_json::to_string(&kind).unwrap();
1739        let back: EventKind = serde_json::from_str(&json).unwrap();
1740        assert!(matches!(
1741            back,
1742            EventKind::HiveTaskCompleted {
1743                total_generations: 5,
1744                ..
1745            }
1746        ));
1747    }
1748
1749    #[test]
1750    fn hive_full_envelope_roundtrip() {
1751        let envelope = make_envelope(EventKind::HiveTaskCreated {
1752            hive_task_id: HiveTaskId::from_string("HIVE-ENV"),
1753            objective: "test envelope".into(),
1754            agent_count: 5,
1755        });
1756        let json = serde_json::to_string(&envelope).unwrap();
1757        let back: EventEnvelope = serde_json::from_str(&json).unwrap();
1758        assert!(matches!(
1759            back.kind,
1760            EventKind::HiveTaskCreated { agent_count: 5, .. }
1761        ));
1762    }
1763
1764    #[test]
1765    fn voice_events_roundtrip() {
1766        let kind = EventKind::VoiceSessionStarted {
1767            voice_session_id: "vs1".into(),
1768            adapter: "openai-realtime".into(),
1769            model: "gpt-4o-realtime".into(),
1770            sample_rate_hz: 24000,
1771            channels: 1,
1772        };
1773        let json = serde_json::to_string(&kind).unwrap();
1774        let back: EventKind = serde_json::from_str(&json).unwrap();
1775        assert!(matches!(back, EventKind::VoiceSessionStarted { .. }));
1776    }
1777}