bamboo_engine/external_agents/
actor_event_stream.rs1use serde::Serialize;
6use sha2::{Digest, Sha256};
7use std::fmt::Write;
8
9use super::actor_event_router::{ActorEventClass, ActorEventEnvelope};
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
12#[serde(rename_all = "snake_case")]
13pub enum PublicActorEventClass {
14 Lifecycle,
15 Semantic,
16 Snapshot,
17 Ephemeral,
18}
19
20#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
24pub struct ActorEventSourceOrder {
25 pub lease_epoch: u64,
26 pub execution_epoch: u64,
27 pub sequence: u64,
28}
29
30#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
33pub struct PublicActorEvent {
34 pub actor_id: String,
35 pub root_actor_id: String,
36 pub parent_actor_id: Option<String>,
37 pub activation_id: String,
38 pub attempt: u64,
39 pub event_id: String,
40 pub class: PublicActorEventClass,
41 #[serde(skip)]
42 pub source_order: ActorEventSourceOrder,
43}
44
45pub(super) fn opaque_event_id(identity: &str) -> String {
46 let digest = Sha256::digest(identity.as_bytes());
47 let mut event_id = String::with_capacity(68);
48 event_id.push_str("ae1-");
49 for byte in digest {
50 write!(&mut event_id, "{byte:02x}").expect("String formatting cannot fail");
51 }
52 event_id
53}
54
55impl From<&ActorEventEnvelope> for PublicActorEvent {
56 fn from(envelope: &ActorEventEnvelope) -> Self {
57 let event_id = opaque_event_id(&envelope.event_id);
58 let class = match envelope.class {
59 ActorEventClass::Lifecycle => PublicActorEventClass::Lifecycle,
60 ActorEventClass::Semantic => PublicActorEventClass::Semantic,
61 ActorEventClass::Snapshot => PublicActorEventClass::Snapshot,
62 ActorEventClass::Ephemeral => PublicActorEventClass::Ephemeral,
63 };
64 Self {
65 actor_id: envelope.actor_id.clone(),
66 root_actor_id: envelope.root_actor_id.clone(),
67 parent_actor_id: envelope.parent_actor_id.clone(),
68 activation_id: envelope.activation_id.clone(),
69 attempt: envelope.attempt,
70 event_id,
71 class,
72 source_order: ActorEventSourceOrder {
73 lease_epoch: envelope.lease_epoch,
74 execution_epoch: envelope.execution_epoch,
75 sequence: envelope.sequence,
76 },
77 }
78 }
79}
80
81pub trait ActorEventObserver: Send + Sync {
84 fn publish(&self, event: PublicActorEvent);
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use bamboo_agent_core::AgentEvent;
91
92 #[test]
93 fn public_projection_hashes_internal_identity_and_excludes_content() {
94 let envelope = ActorEventEnvelope {
95 actor_id: "child".into(),
96 parent_actor_id: Some("root".into()),
97 root_actor_id: "root".into(),
98 activation_id: "safe-activation".into(),
99 attempt: 1,
100 lease_epoch: 7,
101 execution_epoch: 9,
102 sequence: 3,
103 event_id: "safe-activation:7:9:3".into(),
104 class: ActorEventClass::Semantic,
105 payload: AgentEvent::Token {
106 content: "private-token".into(),
107 },
108 };
109 let public = PublicActorEvent::from(&envelope);
110 assert_eq!(public.event_id.len(), 68);
111 assert!(public.event_id.starts_with("ae1-"));
112 assert_eq!(public.source_order.sequence, 3);
113 assert!(public.event_id[4..]
114 .bytes()
115 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)));
116 let json = serde_json::to_string(&public).unwrap();
117 for private in [
118 "private-token",
119 "project_id",
120 "lease_epoch",
121 "execution_epoch",
122 "source_order",
123 ] {
124 assert!(!json.contains(private));
125 }
126 }
127}