Skip to main content

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}