agent_frame/event.rs
1//! Causal events over the existing derivation kernel.
2//!
3//! [`crate::Packet`] keeps its single-predecessor publication chain. An event
4//! has a separate causal parent set, so a reply can name several antecedents
5//! without changing what a packet's `prior` means. A store links the two in its
6//! publication record. Elision events reference the existing [`Unit`] proof;
7//! they do not mint a competing representation of a derivation.
8//!
9//! Derivation sources are an explicit subset of causal parents. Only those
10//! sources determine generation depth: receiving a reply after a harness nudge
11//! is a new external observation, while retrieving that nudge preserves its
12//! harness origin and generation depth. Both construction and foreign decoding
13//! resolve and verify every parent before admitting an immutable [`Event`].
14//!
15//! Admission establishes structure and provenance claims, not the truth of a
16//! generated verdict or the authenticity of an external observation. The host
17//! must own observation recording and must resolve roots, retain/check payload
18//! bytes, and enforce session authorization. A resolver must expose only the
19//! caller's authorized events; content identity alone grants no read authority.
20
21use std::collections::{BTreeMap, BTreeSet};
22
23use content_addressable::{ContentAddressable, ContentError, ContentId, MerkleNode, RawContentId};
24use serde::{Deserialize, Serialize};
25
26use crate::{verify_unit, Op, Unit, UnitId, Verified, VerifyError};
27
28/// Who produced the material, independently of who later retrieved it.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum EventOrigin {
32 /// The operator supplied the material.
33 Operator,
34 /// The primary model supplied the material.
35 Model,
36 /// A tool execution supplied the material.
37 Tool,
38 /// Imported historical data, not a witnessed action by any current actor.
39 Historical,
40 /// The harness supplied the material, including auxiliary model output.
41 Harness,
42}
43
44/// The auxiliary classifier's claim about a recorded model reply.
45///
46/// Admission can check this closed vocabulary and its reply reference. It
47/// cannot establish that the classifier's claim is true or the task is complete.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(rename_all = "snake_case")]
50pub enum ReplyVerdict {
51 /// The reply presents an answer or deliverable.
52 Answer,
53 /// The reply narrates intended work.
54 Narration,
55 /// The reply asks the operator a question.
56 Question,
57}
58
59/// The event's operation; operation-specific references cannot be omitted.
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case", deny_unknown_fields)]
62pub enum EventKind {
63 /// External material recorded by the host, with no derivation sources.
64 Observation,
65 /// A generated harness instruction, rooted in the event's explicit root.
66 Intervention,
67 /// A classification of exactly one recorded model observation.
68 Verdict {
69 /// The classifier's proposed class.
70 verdict: ReplyVerdict,
71 },
72 /// A read of one existing artifact, retaining its depth and origin.
73 Retrieval,
74 /// A pointer backed by the kernel's existing addressed elision proof.
75 Elision {
76 /// The admitted unit that addresses the removed source span.
77 unit: UnitId,
78 },
79}
80
81/// The addressed event body. Public fields are untrusted until admission.
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(deny_unknown_fields)]
84pub struct EventBody {
85 /// The triggering [`crate::RootEvent`], resolved by the storage layer.
86 pub root: ContentId,
87 /// Origin of the material; retrieval and elision inherit it unchanged.
88 pub origin: EventOrigin,
89 /// The operation and any operation-specific claim.
90 pub kind: EventKind,
91 /// Retained event payload bytes, verified by their storage consumer.
92 pub payload: RawContentId,
93 /// Session-local occurrence ordinal, distinguishing identical observations.
94 pub seq: u64,
95 /// Derivation inputs, distinct from other causal antecedents.
96 pub sources: BTreeSet<ContentId>,
97 /// Claimed generation depth; admission recomputes it from resolved inputs.
98 pub depth: u32,
99}
100
101/// Foreign bytes decode to this untrusted Merkle node before [`Event::admit`].
102pub type RawEvent = MerkleNode<EventBody>;
103
104/// An immutable event whose parent membership and derivation laws were checked.
105#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
106#[serde(transparent)]
107pub struct Event(RawEvent);
108
109/// Why an event or its elision proof was refused.
110#[derive(Debug, thiserror::Error)]
111pub enum EventError {
112 /// An event link did not resolve in the caller's authorized graph.
113 #[error("event reference is absent or unauthorized: {0}")]
114 MissingReference(Box<ContentId>),
115 /// A resolver returned an event filed under another event's address.
116 #[error("event reference {expected} resolves to {actual}")]
117 ReferenceMismatch {
118 /// The requested content address.
119 expected: Box<ContentId>,
120 /// The returned event's recomputed address.
121 actual: Box<ContentId>,
122 },
123 /// An origin, operation, or reference relationship violates the contract.
124 #[error("invalid event: {0}")]
125 Invalid(&'static str),
126 /// The claimed depth disagrees with the resolved derivation inputs.
127 #[error("event declares depth {declared}, but its derivation implies {implied}")]
128 DepthMismatch {
129 /// The depth supplied by the caller.
130 declared: u32,
131 /// The depth recomputed by admission.
132 implied: u32,
133 },
134 /// Generating from an already generated artifact exceeds the depth bound.
135 #[error("event generation depth {0} exceeds the bound of one")]
136 DepthExceeded(u32),
137 /// The addressing crate could not encode an event.
138 #[error(transparent)]
139 Content(#[from] Box<ContentError>),
140 /// Existing kernel verification rejected the addressed source material.
141 #[error(transparent)]
142 Elision(#[from] VerifyError),
143}
144
145impl From<ContentError> for EventError {
146 fn from(error: ContentError) -> Self {
147 Self::Content(Box::new(error))
148 }
149}
150
151impl Event {
152 /// Construct an event through the same boundary used for foreign bytes.
153 ///
154 /// `resolve` supplies already admitted, authorized events. Root and payload
155 /// resolution remain the storage layer's job; see the module's limits.
156 ///
157 /// # Errors
158 ///
159 /// Returns [`EventError`] for missing/substituted links or invalid derivation.
160 pub fn new(
161 body: EventBody,
162 parents: impl IntoIterator<Item = ContentId>,
163 resolve: impl Fn(&ContentId) -> Option<Event>,
164 ) -> Result<Self, EventError> {
165 Self::admit(MerkleNode::new(body, parents), resolve)
166 }
167
168 /// Admit a decoded event after resolving and re-addressing every parent.
169 ///
170 /// This checks one event against admitted parents, not an unbounded graph
171 /// traversal. A durable reader must restore the closure in dependency order.
172 /// An intervention with no derivation sources is generated from its explicit
173 /// root at depth one; causal predecessors do not become its evidence.
174 ///
175 /// # Errors
176 ///
177 /// Returns [`EventError`] if links, origins, source relations, or depth fail.
178 pub fn admit(
179 raw: RawEvent,
180 resolve: impl Fn(&ContentId) -> Option<Event>,
181 ) -> Result<Self, EventError> {
182 let body = raw.payload();
183 if !body.sources.is_subset(raw.parents()) {
184 return Err(EventError::Invalid(
185 "derivation sources must also be causal parents",
186 ));
187 }
188 let mut parents = BTreeMap::new();
189 for id in raw.parents() {
190 let parent = resolve(id).ok_or_else(|| EventError::MissingReference(Box::new(*id)))?;
191 let actual = parent.id()?;
192 if actual != *id {
193 return Err(EventError::ReferenceMismatch {
194 expected: Box::new(*id),
195 actual: Box::new(actual),
196 });
197 }
198 parents.insert(*id, parent);
199 }
200 let source_depth = body
201 .sources
202 .iter()
203 .map(|id| parents[id].depth())
204 .max()
205 .unwrap_or(0);
206 let implied = match &body.kind {
207 EventKind::Observation => {
208 if body.origin == EventOrigin::Harness || !body.sources.is_empty() {
209 return Err(EventError::Invalid(
210 "observations must be external and have no derivation sources",
211 ));
212 }
213 0
214 }
215 EventKind::Intervention => {
216 require_harness(body)?;
217 Op::Generate.depth_after(source_depth)
218 }
219 EventKind::Verdict { .. } => {
220 require_harness(body)?;
221 let source = single_source(body, &parents)?;
222 if parents.len() != 1
223 || source.body().origin != EventOrigin::Model
224 || source.body().kind != EventKind::Observation
225 {
226 return Err(EventError::Invalid(
227 "a verdict must name exactly one model reply observation as its parent",
228 ));
229 }
230 Op::Generate.depth_after(source_depth)
231 }
232 EventKind::Retrieval | EventKind::Elision { .. } => {
233 let source = single_source(body, &parents)?;
234 if body.origin != source.body().origin {
235 return Err(EventError::Invalid(
236 "retrieval and elision must preserve source origin",
237 ));
238 }
239 Op::Elide.depth_after(source_depth)
240 }
241 };
242 if implied > 1 {
243 return Err(EventError::DepthExceeded(implied));
244 }
245 if body.depth != implied {
246 return Err(EventError::DepthMismatch {
247 declared: body.depth,
248 implied,
249 });
250 }
251 Ok(Self(raw))
252 }
253
254 /// The event's admitted body, borrowed immutably.
255 #[must_use]
256 pub fn body(&self) -> &EventBody {
257 self.0.payload()
258 }
259
260 /// Causal parents, separate from the publication packet's predecessor.
261 #[must_use]
262 pub fn parents(&self) -> &BTreeSet<ContentId> {
263 self.0.parents()
264 }
265
266 /// The depth established by admission.
267 #[must_use]
268 pub fn depth(&self) -> u32 {
269 self.body().depth
270 }
271
272 /// Address of the complete event body and causal parent set.
273 ///
274 /// # Errors
275 ///
276 /// Propagates canonical encoding errors.
277 pub fn id(&self) -> Result<ContentId, ContentError> {
278 self.0.id()
279 }
280
281 /// Clone the transparent encoding; modifications must pass admission again.
282 #[must_use]
283 pub fn to_raw(&self) -> RawEvent {
284 self.0.clone()
285 }
286
287 /// Verify an elision's existing unit proof against its linked source event.
288 ///
289 /// The unit proves the addressed byte span. The event adds inherited origin
290 /// and depth, which v0's byte-only unit cannot determine. This checks neither
291 /// the pointer's rendering nor the root's authority; the consumer owns both.
292 ///
293 /// # Errors
294 ///
295 /// Refuses mismatched unit/source/root links, then propagates [`verify_unit`].
296 pub fn verify_elision(
297 &self,
298 unit: &Unit,
299 source: &Event,
300 bytes: &[u8],
301 ) -> Result<Verified, EventError> {
302 let EventKind::Elision { unit: expected } = &self.body().kind else {
303 return Err(EventError::Invalid("event is not an elision"));
304 };
305 if *expected != unit.id()?
306 || unit.root() != self.body().root
307 || !self.body().sources.contains(&source.id()?)
308 || source.body().payload != unit.derivation().source
309 {
310 return Err(EventError::Invalid(
311 "elision unit, source, or root does not match the event",
312 ));
313 }
314 Ok(verify_unit(unit, bytes)?)
315 }
316}
317
318fn require_harness(body: &EventBody) -> Result<(), EventError> {
319 if body.origin != EventOrigin::Harness {
320 return Err(EventError::Invalid(
321 "generated interventions and verdicts must have harness origin",
322 ));
323 }
324 Ok(())
325}
326
327fn single_source<'a>(
328 body: &EventBody,
329 parents: &'a BTreeMap<ContentId, Event>,
330) -> Result<&'a Event, EventError> {
331 if body.sources.len() != 1 {
332 return Err(EventError::Invalid(
333 "operation requires exactly one derivation source",
334 ));
335 }
336 Ok(&parents[body.sources.first().expect("source cardinality checked")])
337}
338
339impl ContentAddressable for Event {
340 fn canonical_form(&self) -> Result<Vec<u8>, ContentError> {
341 self.0.canonical_form()
342 }
343}