1use std::collections::BTreeMap;
17
18use serde::{Deserialize, Serialize};
19use serde_json::Value;
20use time::format_description::well_known::Rfc3339;
21use time::OffsetDateTime;
22
23pub type EventId = u64;
26
27#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(tag = "kind", rename_all = "snake_case")]
33pub enum SessionEventKind {
34 Message,
35 ToolCall,
36 ToolResult,
37 Plan,
38 Compaction,
39 SystemReminder,
40 Hypothesis,
41 Receipt,
42 Reminder,
43 PermissionDecision,
44 Custom {
45 #[serde(rename = "type")]
46 custom_type: String,
47 },
48}
49
50impl SessionEventKind {
51 pub fn discriminator(&self) -> &str {
53 match self {
54 Self::Message => "message",
55 Self::ToolCall => "tool_call",
56 Self::ToolResult => "tool_result",
57 Self::Plan => "plan",
58 Self::Compaction => "compaction",
59 Self::SystemReminder => "system_reminder",
60 Self::Hypothesis => "hypothesis",
61 Self::Receipt => "receipt",
62 Self::Reminder => "reminder",
63 Self::PermissionDecision => "permission_decision",
64 Self::Custom { custom_type } => custom_type.as_str(),
65 }
66 }
67}
68
69#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
71pub struct AppendEvent {
72 pub kind: SessionEventKind,
73 #[serde(default)]
76 pub payload: Value,
77 #[serde(default)]
80 pub parent_event_id: Option<EventId>,
81 #[serde(default)]
83 pub actor: Option<String>,
84 #[serde(default)]
86 pub tags: Vec<String>,
87 #[serde(default)]
89 pub headers: BTreeMap<String, String>,
90}
91
92impl AppendEvent {
93 pub fn new(kind: SessionEventKind, payload: Value) -> Self {
94 Self {
95 kind,
96 payload,
97 parent_event_id: None,
98 actor: None,
99 tags: Vec::new(),
100 headers: BTreeMap::new(),
101 }
102 }
103
104 pub fn with_actor(mut self, actor: impl Into<String>) -> Self {
105 self.actor = Some(actor.into());
106 self
107 }
108
109 pub fn with_parent(mut self, parent_event_id: EventId) -> Self {
110 self.parent_event_id = Some(parent_event_id);
111 self
112 }
113
114 pub fn with_tags<I, S>(mut self, tags: I) -> Self
115 where
116 I: IntoIterator<Item = S>,
117 S: Into<String>,
118 {
119 self.tags = tags.into_iter().map(Into::into).collect();
120 self
121 }
122
123 pub fn with_identity(
125 mut self,
126 identity: &crate::identity::EventIdentity,
127 ) -> Result<Self, crate::identity::EventIdentityError> {
128 identity.apply_to_headers(&mut self.headers)?;
129 Ok(self)
130 }
131
132 pub fn identity(
134 &self,
135 ) -> Result<crate::identity::EventIdentity, crate::identity::EventIdentityError> {
136 crate::identity::EventIdentity::from_headers(&self.headers)
137 }
138}
139
140#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
143pub struct StoredEvent {
144 pub event_id: EventId,
145 pub session_id: String,
146 pub tenant_id: Option<String>,
147 pub parent_event_id: Option<EventId>,
148 pub actor: Option<String>,
149 pub kind: SessionEventKind,
150 pub payload: Value,
151 pub tags: Vec<String>,
152 pub headers: BTreeMap<String, String>,
153 pub ts_ms: i64,
154 pub ts: String,
155 pub record_hash: String,
161 pub prev_hash: Option<String>,
164 pub signed_by: Option<EventSignature>,
167}
168
169impl StoredEvent {
170 pub fn identity(
172 &self,
173 ) -> Result<crate::identity::EventIdentity, crate::identity::EventIdentityError> {
174 crate::identity::EventIdentity::from_headers(&self.headers)
175 }
176
177 pub fn is_redacted_projection(&self) -> bool {
181 self.record_hash.starts_with("redacted:")
182 }
183
184 pub fn source_record_hash(&self) -> &str {
187 self.record_hash
188 .strip_prefix("redacted:")
189 .unwrap_or(&self.record_hash)
190 }
191
192 pub(crate) fn mark_redacted_projection(&mut self) {
193 if !self.is_redacted_projection() {
194 self.record_hash = format!("redacted:{}", self.record_hash);
195 }
196 self.signed_by = None;
197 }
198}
199
200#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
201pub struct EventSignature {
202 pub algorithm: String,
204 pub key_id: String,
206 pub signature: String,
208}
209
210pub fn canonical_event_bytes(event: &StoredEvent) -> Vec<u8> {
213 let canonical = serde_json::json!({
214 "schema": "harn.session.event.v1",
215 "session_id": event.session_id,
216 "event_id": event.event_id,
217 "tenant_id": event.tenant_id,
218 "parent_event_id": event.parent_event_id,
219 "actor": event.actor,
220 "kind": event.kind.discriminator(),
221 "payload": event.payload,
222 "tags": event.tags,
223 "headers": event.headers,
224 "ts_ms": event.ts_ms,
225 "prev_hash": event.prev_hash,
226 });
227 canonical_json_bytes(&canonical)
228}
229
230pub fn canonical_json_bytes(value: &Value) -> Vec<u8> {
232 let mut buf = Vec::new();
233 write_canonical(&mut buf, value);
234 buf
235}
236
237fn write_canonical(buf: &mut Vec<u8>, value: &Value) {
238 match value {
239 Value::Null => buf.extend_from_slice(b"null"),
240 Value::Bool(b) => buf.extend_from_slice(if *b { b"true" } else { b"false" }),
241 Value::Number(n) => buf.extend_from_slice(n.to_string().as_bytes()),
242 Value::String(s) => {
243 let encoded = serde_json::to_string(s).expect("string round-trip");
244 buf.extend_from_slice(encoded.as_bytes());
245 }
246 Value::Array(items) => {
247 buf.push(b'[');
248 for (i, item) in items.iter().enumerate() {
249 if i > 0 {
250 buf.push(b',');
251 }
252 write_canonical(buf, item);
253 }
254 buf.push(b']');
255 }
256 Value::Object(map) => {
257 let mut keys: Vec<&String> = map.keys().collect();
258 keys.sort();
259 buf.push(b'{');
260 for (i, key) in keys.iter().enumerate() {
261 if i > 0 {
262 buf.push(b',');
263 }
264 let encoded_key = serde_json::to_string(key).expect("key round-trip");
265 buf.extend_from_slice(encoded_key.as_bytes());
266 buf.push(b':');
267 write_canonical(buf, &map[*key]);
268 }
269 buf.push(b'}');
270 }
271 }
272}
273
274pub(crate) fn now_ms_and_rfc3339() -> (i64, String) {
275 let now = OffsetDateTime::now_utc();
276 let ms = (now.unix_timestamp_nanos() / 1_000_000) as i64;
277 let text = now.format(&Rfc3339).unwrap_or_else(|_| now.to_string());
278 (ms, text)
279}
280
281pub(crate) fn ms_to_rfc3339(ms: i64) -> String {
285 let nanos = (ms as i128).saturating_mul(1_000_000);
286 OffsetDateTime::from_unix_timestamp_nanos(nanos)
287 .ok()
288 .and_then(|dt| dt.format(&Rfc3339).ok())
289 .unwrap_or_default()
290}