use std::collections::BTreeMap;
use std::fmt;
use std::string::String;
use std::vec::Vec;
use crate::{
capability::Capability,
hash::Hash,
serde_utils::StableMap,
time::LogicalTime,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
pub struct EventId {
pub run_id: u64,
pub sequence: u64,
}
impl EventId {
#[must_use]
pub const fn new(run_id: u64, sequence: u64) -> Self {
Self { run_id, sequence }
}
#[must_use]
pub const fn initial(run_id: u64) -> Self {
Self {
run_id,
sequence: 0,
}
}
#[must_use]
pub const fn next(&self) -> Self {
Self {
run_id: self.run_id,
sequence: self.sequence + 1,
}
}
#[must_use]
pub const fn to_logical_time(&self) -> LogicalTime {
LogicalTime {
run_id: self.run_id,
sequence: self.sequence,
}
}
#[must_use]
pub const fn run_id(&self) -> u64 {
self.run_id
}
#[must_use]
pub const fn sequence(&self) -> u64 {
self.sequence
}
}
impl fmt::Display for EventId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "E({}:{})", self.run_id, self.sequence)
}
}
impl From<LogicalTime> for EventId {
fn from(t: LogicalTime) -> Self {
Self {
run_id: t.run_id,
sequence: t.sequence,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum EventKind {
AgentInit,
StateTransition,
ToolRequest,
ToolResponse,
CapabilityDenied,
Observation,
Decision,
MemoryWrite,
MemoryRead,
PatchProposal,
PatchApplied,
PatchRejected,
Error,
Snapshot,
Custom(String),
}
impl EventKind {
#[must_use]
pub fn as_str(&self) -> &str {
match self {
EventKind::AgentInit => "agent_init",
EventKind::StateTransition => "state_transition",
EventKind::ToolRequest => "tool_request",
EventKind::ToolResponse => "tool_response",
EventKind::CapabilityDenied => "capability_denied",
EventKind::Observation => "observation",
EventKind::Decision => "decision",
EventKind::MemoryWrite => "memory_write",
EventKind::MemoryRead => "memory_read",
EventKind::PatchProposal => "patch_proposal",
EventKind::PatchApplied => "patch_applied",
EventKind::PatchRejected => "patch_rejected",
EventKind::Error => "error",
EventKind::Snapshot => "snapshot",
EventKind::Custom(s) => s,
}
}
}
impl fmt::Display for EventKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Event {
pub id: EventId,
pub parent_id: Option<EventId>,
pub kind: EventKind,
pub timestamp: LogicalTime,
pub payload: EventPayload,
pub payload_hash: Hash,
pub state_hash_before: Option<Hash>,
pub state_hash_after: Option<Hash>,
}
impl Event {
#[must_use]
pub fn new(
id: EventId,
kind: EventKind,
timestamp: LogicalTime,
payload: EventPayload,
) -> Self {
let payload_hash = payload.hash();
Self {
id,
parent_id: None,
kind,
timestamp,
payload,
payload_hash,
state_hash_before: None,
state_hash_after: None,
}
}
#[must_use]
pub fn with_parent(
id: EventId,
parent_id: EventId,
kind: EventKind,
timestamp: LogicalTime,
payload: EventPayload,
) -> Self {
let payload_hash = payload.hash();
Self {
id,
parent_id: Some(parent_id),
kind,
timestamp,
payload,
payload_hash,
state_hash_before: None,
state_hash_after: None,
}
}
#[must_use]
pub fn with_state_hashes(mut self, before: Hash, after: Hash) -> Self {
self.state_hash_before = Some(before);
self.state_hash_after = Some(after);
self
}
#[must_use]
pub fn event_hash(&self) -> Hash {
Hash::from_canonical(self)
}
#[must_use]
pub fn verify_payload_hash(&self) -> bool {
self.payload_hash == self.payload.hash()
}
#[must_use]
pub fn follows(&self, parent: &EventId) -> bool {
self.parent_id.as_ref() == Some(parent)
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum EventPayload {
AgentInit(AgentInitPayload),
StateTransition(StateTransitionPayload),
ToolRequest(ToolRequestPayload),
ToolResponse(ToolResponsePayload),
CapabilityDenied(CapabilityDeniedPayload),
Observation(ObservationPayload),
Decision(DecisionPayload),
MemoryWrite(MemoryPayload),
MemoryRead(MemoryPayload),
PatchProposal(PatchPayload),
PatchApplied(PatchPayload),
PatchRejected(PatchRejectedPayload),
Error(ErrorPayload),
Snapshot(SnapshotPayload),
Raw(StableMap<String, String>),
}
impl EventPayload {
#[must_use]
pub fn hash(&self) -> Hash {
Hash::from_canonical(self)
}
#[must_use]
pub fn kind(&self) -> EventKind {
match self {
EventPayload::AgentInit(_) => EventKind::AgentInit,
EventPayload::StateTransition(_) => EventKind::StateTransition,
EventPayload::ToolRequest(_) => EventKind::ToolRequest,
EventPayload::ToolResponse(_) => EventKind::ToolResponse,
EventPayload::CapabilityDenied(_) => EventKind::CapabilityDenied,
EventPayload::Observation(_) => EventKind::Observation,
EventPayload::Decision(_) => EventKind::Decision,
EventPayload::MemoryWrite(_) => EventKind::MemoryWrite,
EventPayload::MemoryRead(_) => EventKind::MemoryRead,
EventPayload::PatchProposal(_) => EventKind::PatchProposal,
EventPayload::PatchApplied(_) => EventKind::PatchApplied,
EventPayload::PatchRejected(_) => EventKind::PatchRejected,
EventPayload::Error(_) => EventKind::Error,
EventPayload::Snapshot(_) => EventKind::Snapshot,
EventPayload::Raw(_) => EventKind::Custom("raw".to_string()),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct AgentInitPayload {
pub agent_type: String,
pub agent_version: String,
pub config: StableMap<String, String>,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct StateTransitionPayload {
pub from_hash: Hash,
pub to_hash: Hash,
pub transition_type: String,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ToolRequestPayload {
pub tool_name: String,
pub tool_version: String,
pub request_hash: Hash,
pub capabilities: Vec<Capability>,
pub input: String,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ToolResponsePayload {
pub tool_name: String,
pub request_hash: Hash,
pub response_hash: Hash,
pub output: String,
pub success: bool,
pub error: Option<String>,
pub duration_ms: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct CapabilityDeniedPayload {
pub capability: Capability,
pub tool_name: String,
pub reason: String,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ObservationPayload {
pub obs_type: String,
pub data: StableMap<String, String>,
pub source: String,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct DecisionPayload {
pub decision_type: String,
pub data: StableMap<String, String>,
pub reasoning: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct MemoryPayload {
pub operation: String,
pub key: String,
pub value_hash: Option<Hash>,
pub causal_event: EventId,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct PatchPayload {
pub patch_type: String,
pub target: String,
pub patch_hash: Hash,
pub reasoning: String,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct PatchRejectedPayload {
pub patch_hash: Hash,
pub reason: String,
pub stage: String,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ErrorPayload {
pub error_type: String,
pub message: String,
pub component: String,
pub recoverable: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SnapshotPayload {
pub snapshot_id: String,
pub at_sequence: u64,
pub state_hash: Hash,
pub events_before: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct EventLog {
pub run_id: u64,
events: Vec<Event>,
index: BTreeMap<EventId, usize>,
}
impl EventLog {
#[must_use]
pub fn new(run_id: u64) -> Self {
Self {
run_id,
events: Vec::new(),
index: BTreeMap::new(),
}
}
pub fn append(&mut self, event: Event) -> Result<(), EventLogError> {
if event.id.run_id != self.run_id {
return Err(EventLogError::CorruptedLog(format!(
"Event run_id mismatch: expected {}, got {}",
self.run_id, event.id.run_id
)));
}
let expected_seq = self.events.len() as u64;
if event.id.sequence != expected_seq {
return Err(EventLogError::CorruptedLog(format!(
"Event sequence mismatch: expected {}, got {}",
expected_seq, event.id.sequence
)));
}
if let Some(parent) = event.parent_id {
if !self.index.contains_key(&parent) {
return Err(EventLogError::ParentNotFound(parent.to_string()));
}
}
if !event.verify_payload_hash() {
return Err(EventLogError::HashMismatch {
expected: event.payload_hash.to_hex(),
actual: event.payload.hash().to_hex(),
});
}
let idx = self.events.len();
self.index.insert(event.id, idx);
self.events.push(event);
Ok(())
}
#[must_use]
pub fn get(&self, id: EventId) -> Option<&Event> {
self.index.get(&id).and_then(|&idx| self.events.get(idx))
}
#[must_use]
pub fn get_by_sequence(&self, seq: u64) -> Option<&Event> {
self.events.get(seq as usize)
}
#[must_use]
pub fn last(&self) -> Option<&Event> {
self.events.last()
}
#[must_use]
pub fn events(&self) -> &[Event] {
&self.events
}
#[must_use]
pub fn len(&self) -> usize {
self.events.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.events.is_empty()
}
#[must_use]
pub fn snapshot(&self) -> EventLogSnapshot {
EventLogSnapshot {
run_id: self.run_id,
at_sequence: self.len() as u64,
last_event_id: self.last().map(|e| e.id),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct EventLogSnapshot {
pub run_id: u64,
pub at_sequence: u64,
pub last_event_id: Option<EventId>,
}
impl EventLogSnapshot {
#[must_use]
pub fn from_log(log: &EventLog) -> Self {
log.snapshot()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EventLogError {
InvalidEventId(String),
ParentNotFound(String),
HashMismatch { expected: String, actual: String },
CorruptedLog(String),
}
impl fmt::Display for EventLogError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EventLogError::InvalidEventId(id) => write!(f, "Invalid event ID: {}", id),
EventLogError::ParentNotFound(id) => write!(f, "Parent not found: {}", id),
EventLogError::HashMismatch { expected, actual } => {
write!(f, "Hash mismatch: expected {}, got {}", expected, actual)
}
EventLogError::CorruptedLog(msg) => write!(f, "Corrupted log: {}", msg),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_event_id() {
let id = EventId::new(1, 0);
assert_eq!(id.run_id(), 1);
assert_eq!(id.sequence(), 0);
assert_eq!(id.next().sequence(), 1);
}
#[test]
fn test_event_log_append() {
let mut log = EventLog::new(42);
let event = Event::new(
EventId::new(42, 0),
EventKind::AgentInit,
LogicalTime::initial(42),
EventPayload::AgentInit(AgentInitPayload {
agent_type: "test".to_string(),
agent_version: "1.0.0".to_string(),
config: StableMap::new(),
}),
);
assert!(log.append(event).is_ok());
assert_eq!(log.len(), 1);
}
#[test]
fn test_event_log_sequence_mismatch() {
let mut log = EventLog::new(42);
let event1 = Event::new(
EventId::new(42, 0),
EventKind::AgentInit,
LogicalTime::initial(42),
EventPayload::AgentInit(AgentInitPayload {
agent_type: "test".to_string(),
agent_version: "1.0.0".to_string(),
config: StableMap::new(),
}),
);
log.append(event1).unwrap();
let event2 = Event::new(
EventId::new(42, 2), EventKind::Observation,
LogicalTime::new(42, 2),
EventPayload::Observation(ObservationPayload {
obs_type: "test".to_string(),
data: StableMap::new(),
source: "test".to_string(),
}),
);
assert!(log.append(event2).is_err());
}
#[test]
fn test_event_with_parent() {
let mut log = EventLog::new(42);
let parent = Event::new(
EventId::new(42, 0),
EventKind::AgentInit,
LogicalTime::initial(42),
EventPayload::AgentInit(AgentInitPayload {
agent_type: "test".to_string(),
agent_version: "1.0.0".to_string(),
config: StableMap::new(),
}),
);
log.append(parent).unwrap();
let child = Event::with_parent(
EventId::new(42, 1),
EventId::new(42, 0),
EventKind::Observation,
LogicalTime::new(42, 1),
EventPayload::Observation(ObservationPayload {
obs_type: "test".to_string(),
data: StableMap::new(),
source: "test".to_string(),
}),
);
assert!(log.append(child).is_ok());
assert_eq!(log.len(), 2);
}
}