use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use time::format_description::well_known::Rfc3339;
use time::OffsetDateTime;
pub type EventId = u64;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum SessionEventKind {
Message,
ToolCall,
ToolResult,
Plan,
Compaction,
SystemReminder,
Hypothesis,
Receipt,
Reminder,
PermissionDecision,
Custom {
#[serde(rename = "type")]
custom_type: String,
},
}
impl SessionEventKind {
pub fn discriminator(&self) -> &str {
match self {
Self::Message => "message",
Self::ToolCall => "tool_call",
Self::ToolResult => "tool_result",
Self::Plan => "plan",
Self::Compaction => "compaction",
Self::SystemReminder => "system_reminder",
Self::Hypothesis => "hypothesis",
Self::Receipt => "receipt",
Self::Reminder => "reminder",
Self::PermissionDecision => "permission_decision",
Self::Custom { custom_type } => custom_type.as_str(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AppendEvent {
pub kind: SessionEventKind,
#[serde(default)]
pub payload: Value,
#[serde(default)]
pub parent_event_id: Option<EventId>,
#[serde(default)]
pub actor: Option<String>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub headers: BTreeMap<String, String>,
}
impl AppendEvent {
pub fn new(kind: SessionEventKind, payload: Value) -> Self {
Self {
kind,
payload,
parent_event_id: None,
actor: None,
tags: Vec::new(),
headers: BTreeMap::new(),
}
}
pub fn with_actor(mut self, actor: impl Into<String>) -> Self {
self.actor = Some(actor.into());
self
}
pub fn with_parent(mut self, parent_event_id: EventId) -> Self {
self.parent_event_id = Some(parent_event_id);
self
}
pub fn with_tags<I, S>(mut self, tags: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.tags = tags.into_iter().map(Into::into).collect();
self
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct StoredEvent {
pub event_id: EventId,
pub session_id: String,
pub tenant_id: Option<String>,
pub parent_event_id: Option<EventId>,
pub actor: Option<String>,
pub kind: SessionEventKind,
pub payload: Value,
pub tags: Vec<String>,
pub headers: BTreeMap<String, String>,
pub ts_ms: i64,
pub ts: String,
pub record_hash: String,
pub prev_hash: Option<String>,
pub signed_by: Option<EventSignature>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct EventSignature {
pub algorithm: String,
pub key_id: String,
pub signature: String,
}
pub fn canonical_event_bytes(event: &StoredEvent) -> Vec<u8> {
let canonical = serde_json::json!({
"schema": "harn.session.event.v1",
"session_id": event.session_id,
"event_id": event.event_id,
"tenant_id": event.tenant_id,
"parent_event_id": event.parent_event_id,
"actor": event.actor,
"kind": event.kind.discriminator(),
"payload": event.payload,
"tags": event.tags,
"headers": event.headers,
"ts_ms": event.ts_ms,
"prev_hash": event.prev_hash,
});
canonical_json_bytes(&canonical)
}
pub fn canonical_json_bytes(value: &Value) -> Vec<u8> {
let mut buf = Vec::new();
write_canonical(&mut buf, value);
buf
}
fn write_canonical(buf: &mut Vec<u8>, value: &Value) {
match value {
Value::Null => buf.extend_from_slice(b"null"),
Value::Bool(b) => buf.extend_from_slice(if *b { b"true" } else { b"false" }),
Value::Number(n) => buf.extend_from_slice(n.to_string().as_bytes()),
Value::String(s) => {
let encoded = serde_json::to_string(s).expect("string round-trip");
buf.extend_from_slice(encoded.as_bytes());
}
Value::Array(items) => {
buf.push(b'[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
buf.push(b',');
}
write_canonical(buf, item);
}
buf.push(b']');
}
Value::Object(map) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
buf.push(b'{');
for (i, key) in keys.iter().enumerate() {
if i > 0 {
buf.push(b',');
}
let encoded_key = serde_json::to_string(key).expect("key round-trip");
buf.extend_from_slice(encoded_key.as_bytes());
buf.push(b':');
write_canonical(buf, &map[*key]);
}
buf.push(b'}');
}
}
}
pub(crate) fn now_ms_and_rfc3339() -> (i64, String) {
let now = OffsetDateTime::now_utc();
let ms = (now.unix_timestamp_nanos() / 1_000_000) as i64;
let text = now.format(&Rfc3339).unwrap_or_else(|_| now.to_string());
(ms, text)
}
pub(crate) fn ms_to_rfc3339(ms: i64) -> String {
let nanos = (ms as i128).saturating_mul(1_000_000);
OffsetDateTime::from_unix_timestamp_nanos(nanos)
.ok()
.and_then(|dt| dt.format(&Rfc3339).ok())
.unwrap_or_default()
}