systemprompt_models/brain/
event.rs1use 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#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
21pub struct BrainEventKind(String);
22
23#[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#[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 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 pub fn with_payload(mut self, payload: serde_json::Value) -> Self {
143 self.payload = payload;
144 self
145 }
146}