Skip to main content

polyc_eventlog/
event.rs

1//! The [`Event`] item stored in the log and its [`commonware_codec`] wiring.
2//!
3//! An [`Event`] is the unit the journal persists: a `kind` discriminator plus
4//! an opaque `payload`. This crate is deliberately **payload-agnostic** — the
5//! payload is a `Vec<u8>` of buffa-encoded bytes whose concrete schema lives in
6//! `polyc-proto`'s `events.proto`. The event log neither encodes nor
7//! interprets it; it round-trips the bytes verbatim.
8//!
9//! The journal primitive (`commonware_storage::journal::contiguous::variable`)
10//! requires its item type to implement [`commonware_codec`]'s [`Write`],
11//! [`EncodeSize`], and [`Read`] traits. [`Event`] implements them by length-
12//! prefixing the `kind` UTF-8 bytes and the `payload` bytes, exactly the
13//! variable-size pattern documented in the `commonware-codec` crate.
14
15use bytes::{Buf, BufMut};
16use commonware_codec::{EncodeSize, Error as CodecError, Read, ReadRangeExt as _, Write};
17
18/// A single conversation event: a `kind` tag and an opaque encoded payload.
19///
20/// `kind` is a short string discriminator (e.g. `"user_msg"`,
21/// `"planner_decision"`, `"tool_call"`). `payload` is opaque to this crate —
22/// it is the buffa-encoded body of whichever `events.proto` message the `kind`
23/// names, stored and replayed byte-for-byte.
24///
25/// The position (turn/seq ordering) is **not** carried in the event itself: the
26/// journal assigns each appended event a monotonically increasing position, and
27/// [`crate::EventLog::replay`] yields events in that append order. See the crate
28/// docs for the ordering contract.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct Event {
31    /// Discriminator naming the payload's schema (e.g. `"tool_call"`).
32    pub kind: String,
33    /// Opaque, buffa-encoded payload bytes. Round-tripped verbatim.
34    pub payload: Vec<u8>,
35}
36
37impl Event {
38    /// Construct an [`Event`] from a `kind` and an owned payload byte vector.
39    #[must_use]
40    pub fn new(kind: impl Into<String>, payload: Vec<u8>) -> Self {
41        Self {
42            kind: kind.into(),
43            payload,
44        }
45    }
46}
47
48/// Decode-time bounds for an [`Event`], supplied as the journal's
49/// `codec_config`.
50///
51/// `commonware-codec` requires an explicit maximum length when reading any
52/// variable-length field, so that a corrupt or hostile length prefix cannot
53/// trigger an unbounded allocation. These caps are applied when decoding the
54/// `kind` and `payload` of each event during a [`crate::EventLog::replay`].
55///
56/// [`EventCfg::DEFAULT`] provides generous defaults suitable for conversation
57/// events; tighten them per deployment if desired.
58#[derive(Debug, Clone, Copy)]
59pub struct EventCfg {
60    /// Maximum byte length permitted for a decoded `kind` string.
61    pub max_kind_len: usize,
62    /// Maximum byte length permitted for a decoded `payload`.
63    pub max_payload_len: usize,
64}
65
66impl EventCfg {
67    /// Default decode bounds: 256-byte `kind`, 16 MiB `payload`.
68    pub const DEFAULT: Self = Self {
69        max_kind_len: 256,
70        max_payload_len: 16 * 1024 * 1024,
71    };
72}
73
74impl Default for EventCfg {
75    fn default() -> Self {
76        Self::DEFAULT
77    }
78}
79
80impl Write for Event {
81    fn write(&self, buf: &mut impl BufMut) {
82        // `Vec<u8>: Write` length-prefixes then writes the bytes. We encode the
83        // `kind` as its UTF-8 bytes (a `Vec<u8>` on the wire) and the `payload`
84        // as-is, so decoding is the symmetric `read_range` of each.
85        self.kind.as_bytes().to_vec().write(buf);
86        self.payload.write(buf);
87    }
88}
89
90impl EncodeSize for Event {
91    fn encode_size(&self) -> usize {
92        self.kind.as_bytes().to_vec().encode_size() + self.payload.encode_size()
93    }
94}
95
96impl Read for Event {
97    type Cfg = EventCfg;
98
99    fn read_cfg(buf: &mut impl Buf, cfg: &EventCfg) -> Result<Self, CodecError> {
100        let kind_bytes = <Vec<u8>>::read_range(buf, 0..=cfg.max_kind_len)?;
101        let kind = String::from_utf8(kind_bytes)
102            .map_err(|_| CodecError::Invalid("Event", "kind is not valid UTF-8"))?;
103        let payload = <Vec<u8>>::read_range(buf, 0..=cfg.max_payload_len)?;
104        Ok(Self { kind, payload })
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::{Event, EventCfg};
111    use commonware_codec::{Decode as _, Encode as _};
112
113    #[test]
114    fn round_trips_through_codec() {
115        let event = Event::new("tool_call", vec![1, 2, 3, 0xff, 0]);
116        let bytes = event.encode();
117        let decoded = Event::decode_cfg(bytes, &EventCfg::DEFAULT).expect("decode");
118        assert_eq!(event, decoded);
119    }
120
121    #[test]
122    fn round_trips_empty_payload() {
123        let event = Event::new("user_msg", Vec::new());
124        let bytes = event.encode();
125        let decoded = Event::decode_cfg(bytes, &EventCfg::DEFAULT).expect("decode");
126        assert_eq!(event, decoded);
127        assert!(decoded.payload.is_empty());
128    }
129
130    #[test]
131    fn rejects_payload_over_cap() {
132        let event = Event::new("k", vec![0u8; 64]);
133        let bytes = event.encode();
134        let tight = EventCfg {
135            max_kind_len: 256,
136            max_payload_len: 8,
137        };
138        assert!(Event::decode_cfg(bytes, &tight).is_err());
139    }
140}