返回 CodeWhale
ledger.rs
根目录 / crates / tui / src / core / runtime_contract / ledger.rs
1 use serde::{Deserialize, Serialize};
2 use serde_json::Value;
3 use sha2::{Digest, Sha256};
4
5 use super::RUNTIME_CONTRACT_SCHEMA_VERSION;
6
7 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8 #[serde(rename_all = "snake_case")]
9 pub enum RuntimeEventKind {
10 UserMessage,
11 AssistantMessage,
12 ToolStarted,
13 ToolCompleted,
14 ApprovalRequested,
15 ApprovalResolved,
16 SteeringQueued,
17 SteeringDelivered,
18 ResourcesLoaded,
19 Condensation,
20 Work,
21 Child,
22 Usage,
23 Retry,
24 Termination,
25 }
26
27 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28 pub struct RuntimeEventEnvelope {
29 pub schema_version: u32,
30 pub sequence: u64,
31 pub event_id: String,
32 pub kind: RuntimeEventKind,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub parent_event_id: Option<String>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub causal_event_id: Option<String>,
37 pub recorded_at_ms: u64,
38 pub payload: Value,
39 pub checksum: String,
40 }
41
42 impl RuntimeEventEnvelope {
43 #[must_use]
44 pub fn new(
45 sequence: u64,
46 event_id: impl Into<String>,
47 kind: RuntimeEventKind,
48 recorded_at_ms: u64,
49 payload: Value,
50 ) -> Self {
51 let mut event = Self {
52 schema_version: RUNTIME_CONTRACT_SCHEMA_VERSION,
53 sequence,
54 event_id: event_id.into(),
55 kind,
56 parent_event_id: None,
57 causal_event_id: None,
58 recorded_at_ms,
59 payload,
60 checksum: String::new(),
61 };
62 event.checksum = event.expected_checksum();
63 event
64 }
65
66 #[must_use]
67 pub fn expected_checksum(&self) -> String {
68 let canonical = serde_json::json!({
69 "schema_version": self.schema_version,
70 "sequence": self.sequence,
71 "event_id": self.event_id,
72 "kind": self.kind,
73 "parent_event_id": self.parent_event_id,
74 "causal_event_id": self.causal_event_id,
75 "recorded_at_ms": self.recorded_at_ms,
76 "payload": self.payload,
77 });
78 let bytes = serde_json::to_vec(&canonical).expect("runtime event JSON is serializable");
79 let digest = Sha256::digest(bytes);
80 digest.iter().map(|byte| format!("{byte:02x}")).collect()
81 }
82
83 pub fn validate(&self) -> Result<(), String> {
84 if self.schema_version != RUNTIME_CONTRACT_SCHEMA_VERSION {
85 return Err(format!(
86 "unsupported runtime event schema {}",
87 self.schema_version
88 ));
89 }
90 if self.event_id.trim().is_empty() {
91 return Err("runtime event ID cannot be empty".to_string());
92 }
93 let expected = self.expected_checksum();
94 if self.checksum != expected {
95 return Err(format!("runtime event {} checksum mismatch", self.event_id));
96 }
97 Ok(())
98 }
99 }
100
101 #[derive(Debug, Default, Clone)]
102 pub struct AppendOnlyRuntimeLedger {
103 events: Vec<RuntimeEventEnvelope>,
104 }
105
106 impl AppendOnlyRuntimeLedger {
107 pub fn append(&mut self, event: RuntimeEventEnvelope) -> Result<(), String> {
108 event.validate()?;
109 let expected_sequence = self.events.last().map_or(0, |last| last.sequence + 1);
110 if event.sequence != expected_sequence {
111 return Err(format!(
112 "runtime event sequence {} does not follow {}",
113 event.sequence, expected_sequence
114 ));
115 }
116 if self
117 .events
118 .iter()
119 .any(|item| item.event_id == event.event_id)
120 {
121 return Err(format!("duplicate runtime event ID `{}`", event.event_id));
122 }
123 self.events.push(event);
124 Ok(())
125 }
126
127 #[must_use]
128 pub fn events(&self) -> &[RuntimeEventEnvelope] {
129 &self.events
130 }
131
132 #[must_use]
133 pub fn range(&self, start: u64, end_inclusive: u64) -> Vec<&RuntimeEventEnvelope> {
134 self.events
135 .iter()
136 .filter(|event| (start..=end_inclusive).contains(&event.sequence))
137 .collect()
138 }
139 }
140
141 #[cfg(test)]
142 mod tests {
143 use super::*;
144
145 #[test]
146 fn ledger_rejects_corruption_and_sequence_gaps() {
147 let mut ledger = AppendOnlyRuntimeLedger::default();
148 ledger
149 .append(RuntimeEventEnvelope::new(
150 0,
151 "event-0",
152 RuntimeEventKind::UserMessage,
153 1,
154 serde_json::json!({"text": "hello"}),
155 ))
156 .unwrap();
157
158 let gap = RuntimeEventEnvelope::new(
159 2,
160 "event-2",
161 RuntimeEventKind::Termination,
162 2,
163 serde_json::json!({}),
164 );
165 assert!(ledger.append(gap).unwrap_err().contains("does not follow"));
166
167 let mut corrupt = RuntimeEventEnvelope::new(
168 1,
169 "event-1",
170 RuntimeEventKind::ToolCompleted,
171 2,
172 serde_json::json!({"ok": true}),
173 );
174 corrupt.payload = serde_json::json!({"ok": false});
175 assert!(
176 corrupt
177 .validate()
178 .unwrap_err()
179 .contains("checksum mismatch")
180 );
181 }
182
183 #[test]
184 fn derived_range_does_not_remove_original_events() {
185 let mut ledger = AppendOnlyRuntimeLedger::default();
186 for sequence in 0..3 {
187 ledger
188 .append(RuntimeEventEnvelope::new(
189 sequence,
190 format!("event-{sequence}"),
191 RuntimeEventKind::AssistantMessage,
192 sequence,
193 serde_json::json!({"sequence": sequence}),
194 ))
195 .unwrap();
196 }
197 assert_eq!(ledger.range(1, 2).len(), 2);
198 assert_eq!(ledger.events().len(), 3);
199 }
200 }
201
201 lines RUST