Skip to main content

systemprompt_models/documents/
events.rs

1//! Payloads of the AG-UI custom events the document store emits.
2//!
3//! Each payload carries its event name as `NAME`; [`DocumentEventName`]
4//! enumerates them for routing and subscription filters. The sharing
5//! payloads live in [`super::sharing_events`] and are re-exported here.
6//!
7//! Copyright (c) systemprompt.io — Business Source License 1.1.
8//! See <https://systemprompt.io> for licensing details.
9
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize};
12use systemprompt_identifiers::{
13    ContentHash, DocumentCommentId, DocumentEditSessionId, DocumentId, DocumentNodeId,
14    DocumentProposalId, DocumentRevisionId, DriveDeviceId, UserId,
15};
16
17use super::access::{ActorKind, ProposalStatus, RevisionOriginKind};
18use super::awareness::DocumentAwarenessEvent;
19pub use super::collab_events::{DocumentCollabDegradedEvent, DocumentCollabUpdateEvent};
20use super::comment_events::{
21    DocumentCommentDeletedEvent, DocumentCommentMentionedEvent, DocumentCommentUpdatedEvent,
22};
23use super::drive::{PresenceSource, PresenceState};
24
25pub use super::sharing_events::{
26    DocumentAccessRequestedEvent, DocumentOwnershipTransferredEvent, DocumentSharedEvent,
27    SHARED_EVENT_VERSION,
28};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
31pub enum DocumentEventName {
32    #[serde(rename = "document.revision.created")]
33    RevisionCreated,
34    #[serde(rename = "document.presence")]
35    Presence,
36    #[serde(rename = "document.lock")]
37    Lock,
38    #[serde(rename = "document.conflict")]
39    Conflict,
40    #[serde(rename = "document.collab.update")]
41    CollabUpdate,
42    #[serde(rename = "document.comment.created")]
43    CommentCreated,
44    #[serde(rename = "document.comment.resolved")]
45    CommentResolved,
46    #[serde(rename = "document.comment.updated")]
47    CommentUpdated,
48    #[serde(rename = "document.comment.deleted")]
49    CommentDeleted,
50    #[serde(rename = "document.comment.mentioned")]
51    CommentMentioned,
52    #[serde(rename = "document.shared")]
53    Shared,
54    #[serde(rename = "document.ownership_transferred")]
55    OwnershipTransferred,
56    #[serde(rename = "document.proposal.opened")]
57    ProposalOpened,
58    #[serde(rename = "document.proposal.accepted")]
59    ProposalAccepted,
60    #[serde(rename = "document.proposal.rejected")]
61    ProposalRejected,
62    #[serde(rename = "document.proposal.superseded")]
63    ProposalSuperseded,
64    #[serde(rename = "document.awareness")]
65    Awareness,
66    #[serde(rename = "document.access_requested")]
67    AccessRequested,
68    #[serde(rename = "document.collab.degraded")]
69    CollabDegraded,
70}
71
72impl DocumentEventName {
73    pub const ALL: [Self; 19] = [
74        Self::RevisionCreated,
75        Self::Presence,
76        Self::Lock,
77        Self::Conflict,
78        Self::CollabUpdate,
79        Self::CommentCreated,
80        Self::CommentResolved,
81        Self::CommentUpdated,
82        Self::CommentDeleted,
83        Self::CommentMentioned,
84        Self::Shared,
85        Self::OwnershipTransferred,
86        Self::ProposalOpened,
87        Self::ProposalAccepted,
88        Self::ProposalRejected,
89        Self::ProposalSuperseded,
90        Self::Awareness,
91        Self::AccessRequested,
92        Self::CollabDegraded,
93    ];
94
95    #[must_use]
96    pub const fn as_str(self) -> &'static str {
97        match self {
98            Self::RevisionCreated => DocumentRevisionCreatedEvent::NAME,
99            Self::Presence => DocumentPresenceEvent::NAME,
100            Self::Lock => DocumentLockEvent::NAME,
101            Self::Conflict => DocumentConflictEvent::NAME,
102            Self::CollabUpdate => DocumentCollabUpdateEvent::NAME,
103            Self::CommentCreated => DocumentCommentCreatedEvent::NAME,
104            Self::CommentResolved => DocumentCommentResolvedEvent::NAME,
105            Self::CommentUpdated => DocumentCommentUpdatedEvent::NAME,
106            Self::CommentDeleted => DocumentCommentDeletedEvent::NAME,
107            Self::CommentMentioned => DocumentCommentMentionedEvent::NAME,
108            Self::Shared => DocumentSharedEvent::NAME,
109            Self::OwnershipTransferred => DocumentOwnershipTransferredEvent::NAME,
110            Self::ProposalOpened => DocumentProposalEvent::OPENED,
111            Self::ProposalAccepted => DocumentProposalEvent::ACCEPTED,
112            Self::ProposalRejected => DocumentProposalEvent::REJECTED,
113            Self::ProposalSuperseded => DocumentProposalEvent::SUPERSEDED,
114            Self::Awareness => DocumentAwarenessEvent::NAME,
115            Self::AccessRequested => DocumentAccessRequestedEvent::NAME,
116            Self::CollabDegraded => DocumentCollabDegradedEvent::NAME,
117        }
118    }
119
120    #[must_use]
121    pub fn from_name(name: &str) -> Option<Self> {
122        Self::ALL.into_iter().find(|event| event.as_str() == name)
123    }
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
127pub struct DocumentRevisionCreatedEvent {
128    pub document_id: DocumentId,
129    pub revision_id: DocumentRevisionId,
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    pub parent_revision_id: Option<DocumentRevisionId>,
132    pub content_sha256: ContentHash,
133    pub author_id: UserId,
134    pub actor_kind: ActorKind,
135    pub origin_kind: RevisionOriginKind,
136}
137
138impl DocumentRevisionCreatedEvent {
139    pub const NAME: &'static str = "document.revision.created";
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
143pub struct DocumentPresenceEvent {
144    pub document_id: DocumentId,
145    pub user_id: UserId,
146    pub state: PresenceState,
147    pub source_kind: PresenceSource,
148    pub source_ref: String,
149    // JSON: editor cursor position; the shape belongs to the client's editor.
150    #[serde(default, skip_serializing_if = "Option::is_none")]
151    pub cursor: Option<serde_json::Value>,
152}
153
154impl DocumentPresenceEvent {
155    pub const NAME: &'static str = "document.presence";
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
159pub struct DocumentLockEvent {
160    pub document_id: DocumentId,
161    pub locked: bool,
162    #[serde(default, skip_serializing_if = "Option::is_none")]
163    pub holder_id: Option<UserId>,
164    #[serde(default, skip_serializing_if = "Option::is_none")]
165    pub edit_session_id: Option<DocumentEditSessionId>,
166}
167
168impl DocumentLockEvent {
169    pub const NAME: &'static str = "document.lock";
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
173pub struct DocumentConflictEvent {
174    pub document_id: DocumentId,
175    pub path: String,
176    pub head_revision_id: DocumentRevisionId,
177    #[serde(default, skip_serializing_if = "Option::is_none")]
178    pub base_revision_id: Option<DocumentRevisionId>,
179    #[serde(default, skip_serializing_if = "Option::is_none")]
180    pub device_id: Option<DriveDeviceId>,
181}
182
183impl DocumentConflictEvent {
184    pub const NAME: &'static str = "document.conflict";
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
188pub struct DocumentCommentCreatedEvent {
189    pub document_id: DocumentId,
190    pub comment_id: DocumentCommentId,
191    pub node_id: DocumentNodeId,
192    #[serde(default, skip_serializing_if = "Option::is_none")]
193    pub parent_id: Option<DocumentCommentId>,
194    pub author_id: UserId,
195    #[serde(default, skip_serializing_if = "Vec::is_empty")]
196    pub mentions: Vec<UserId>,
197}
198
199impl DocumentCommentCreatedEvent {
200    pub const NAME: &'static str = "document.comment.created";
201}
202
203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
204pub struct DocumentCommentResolvedEvent {
205    pub document_id: DocumentId,
206    pub comment_id: DocumentCommentId,
207    pub resolved_by: UserId,
208}
209
210impl DocumentCommentResolvedEvent {
211    pub const NAME: &'static str = "document.comment.resolved";
212}
213
214/// The payload of every `document.proposal.*` event; the event name says
215/// which transition the proposal made and `status` repeats it.
216#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
217pub struct DocumentProposalEvent {
218    pub document_id: DocumentId,
219    pub proposal_id: DocumentProposalId,
220    pub status: ProposalStatus,
221    pub proposer_id: UserId,
222    pub base_revision_id: DocumentRevisionId,
223    pub proposed_revision_id: DocumentRevisionId,
224    #[serde(default, skip_serializing_if = "Option::is_none")]
225    pub reviewer_id: Option<UserId>,
226    #[serde(default, skip_serializing_if = "Option::is_none")]
227    pub landed_revision_id: Option<DocumentRevisionId>,
228    #[serde(default, skip_serializing_if = "Option::is_none")]
229    pub message: Option<String>,
230}
231
232impl DocumentProposalEvent {
233    pub const OPENED: &'static str = "document.proposal.opened";
234    pub const ACCEPTED: &'static str = "document.proposal.accepted";
235    pub const REJECTED: &'static str = "document.proposal.rejected";
236    pub const SUPERSEDED: &'static str = "document.proposal.superseded";
237
238    #[must_use]
239    pub const fn name_of(status: ProposalStatus) -> &'static str {
240        match status {
241            ProposalStatus::Open => Self::OPENED,
242            ProposalStatus::Accepted => Self::ACCEPTED,
243            ProposalStatus::Rejected => Self::REJECTED,
244            ProposalStatus::Superseded => Self::SUPERSEDED,
245        }
246    }
247}