Skip to main content

systemprompt_models/brain/
event.rs

1//! The event a producer appends to the outbox for the brain consumer, and
2//! the kinds the brain reads.
3//!
4//! Copyright (c) systemprompt.io — Business Source License 1.1.
5//! See <https://systemprompt.io> for licensing details.
6
7use std::fmt;
8use std::str::FromStr;
9
10use chrono::{DateTime, Utc};
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13use systemprompt_identifiers::UserId;
14
15pub const BRAIN_CONSUMER: &str = "brain";
16pub const BRAIN_EVENT_VERSION: u32 = 1;
17
18/// A brain event kind: `.`-separated segments of `[a-z0-9_]`, such as
19/// `document.revision.landed`.
20#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
21pub struct BrainEventKind(String);
22
23/// A string that is not a valid [`BrainEventKind`].
24#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
25#[error("invalid brain event kind {value:?}: {reason}")]
26pub struct BrainEventKindError {
27    pub value: String,
28    pub reason: &'static str,
29}
30
31impl BrainEventKind {
32    pub const REVISION_LANDED: &'static str = "document.revision.landed";
33    pub const COMMENT_RESOLVED: &'static str = "document.comment.resolved";
34    pub const FACT_ENTERED: &'static str = "brain.fact.entered";
35    pub const SESSION_TURN_CLOSED: &'static str = "session.turn.closed";
36
37    pub fn new(value: impl Into<String>) -> Result<Self, BrainEventKindError> {
38        let value = value.into();
39        if let Some(reason) = kind_violation(&value) {
40            return Err(BrainEventKindError { value, reason });
41        }
42        Ok(Self(value))
43    }
44
45    #[must_use]
46    pub fn revision_landed() -> Self {
47        Self(Self::REVISION_LANDED.to_owned())
48    }
49
50    #[must_use]
51    pub fn comment_resolved() -> Self {
52        Self(Self::COMMENT_RESOLVED.to_owned())
53    }
54
55    #[must_use]
56    pub fn fact_entered() -> Self {
57        Self(Self::FACT_ENTERED.to_owned())
58    }
59
60    #[must_use]
61    pub fn session_turn_closed() -> Self {
62        Self(Self::SESSION_TURN_CLOSED.to_owned())
63    }
64
65    #[must_use]
66    pub const fn as_str(&self) -> &str {
67        self.0.as_str()
68    }
69}
70
71fn kind_violation(value: &str) -> Option<&'static str> {
72    if value.is_empty() {
73        return Some("must not be empty");
74    }
75    if value.split('.').any(str::is_empty) {
76        return Some("segments must not be empty");
77    }
78    if !value
79        .bytes()
80        .all(|b| b == b'.' || b == b'_' || b.is_ascii_lowercase() || b.is_ascii_digit())
81    {
82        return Some("must contain only lowercase letters, digits, '_' and '.'");
83    }
84    None
85}
86
87impl fmt::Display for BrainEventKind {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        f.write_str(&self.0)
90    }
91}
92
93impl FromStr for BrainEventKind {
94    type Err = BrainEventKindError;
95
96    fn from_str(s: &str) -> Result<Self, Self::Err> {
97        Self::new(s)
98    }
99}
100
101crate::notifications::string_form::string_serde!(BrainEventKind, "BrainEventKind");
102
103/// One thing that happened that the brain may learn from.
104#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
105pub struct BrainEvent {
106    pub kind: BrainEventKind,
107    pub actor: UserId,
108    pub actor_is_agent: bool,
109    pub via_agent: Option<String>,
110    pub occurred_at: DateTime<Utc>,
111    // JSON: the producer's payload is the event's free-form contract field.
112    pub payload: serde_json::Value,
113}
114
115impl BrainEvent {
116    #[must_use]
117    pub const fn new(kind: BrainEventKind, actor: UserId, occurred_at: DateTime<Utc>) -> Self {
118        Self {
119            kind,
120            actor,
121            actor_is_agent: false,
122            via_agent: None,
123            occurred_at,
124            payload: serde_json::Value::Null,
125        }
126    }
127
128    #[must_use]
129    pub const fn with_actor_is_agent(mut self, actor_is_agent: bool) -> Self {
130        self.actor_is_agent = actor_is_agent;
131        self
132    }
133
134    #[must_use]
135    pub fn with_via_agent(mut self, via_agent: impl Into<String>) -> Self {
136        self.via_agent = Some(via_agent.into());
137        self
138    }
139
140    #[must_use]
141    // JSON: the producer's payload is the event's free-form contract field.
142    pub fn with_payload(mut self, payload: serde_json::Value) -> Self {
143        self.payload = payload;
144        self
145    }
146}