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