Skip to main content

systemprompt_models/
issues.rs

1//! Issue report contracts: the bug and feedback reports a user or an agent
2//! files, the context that ties a report to the platform's own traces, and
3//! the triage vocabulary.
4//!
5//! A report carries identifiers, never copied logs: `trace_id`,
6//! `context_id`, `session_id`, `task_id` and `mcp_execution_id` name the
7//! records the platform already keeps, so triage follows them to the trace
8//! and the report holds no secret a log line might.
9//!
10//! Copyright (c) systemprompt.io — Business Source License 1.1.
11//! See <https://systemprompt.io> for licensing details.
12
13use std::fmt;
14use std::str::FromStr;
15
16use chrono::{DateTime, Utc};
17use schemars::JsonSchema;
18use serde::{Deserialize, Serialize};
19use systemprompt_identifiers::{
20    ContentHash, ContextId, DocumentUploadId, IssueReportId, McpExecutionId, SessionId, TaskId,
21    TraceId, UserId,
22};
23
24#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
25#[error("unknown {kind} `{value}`")]
26pub struct UnknownIssueValue {
27    pub kind: &'static str,
28    pub value: String,
29}
30
31macro_rules! issue_enum {
32    ($name:ident, $kind:literal, { $($variant:ident => $wire:literal),+ $(,)? }) => {
33        impl $name {
34            pub const ALL: &'static [Self] = &[$(Self::$variant),+];
35
36            #[must_use]
37            pub const fn as_str(self) -> &'static str {
38                match self {
39                    $(Self::$variant => $wire),+
40                }
41            }
42        }
43
44        impl fmt::Display for $name {
45            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46                f.write_str(self.as_str())
47            }
48        }
49
50        impl FromStr for $name {
51            type Err = UnknownIssueValue;
52
53            fn from_str(s: &str) -> Result<Self, Self::Err> {
54                match s {
55                    $($wire => Ok(Self::$variant),)+
56                    other => Err(UnknownIssueValue {
57                        kind: $kind,
58                        value: other.to_owned(),
59                    }),
60                }
61            }
62        }
63    };
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
67#[serde(rename_all = "snake_case")]
68pub enum IssueKind {
69    Bug,
70    Feedback,
71}
72
73issue_enum!(IssueKind, "issue kind", {
74    Bug => "bug",
75    Feedback => "feedback",
76});
77
78#[derive(
79    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
80)]
81#[serde(rename_all = "snake_case")]
82pub enum IssueSeverity {
83    Low,
84    Medium,
85    High,
86    Critical,
87}
88
89issue_enum!(IssueSeverity, "issue severity", {
90    Low => "low",
91    Medium => "medium",
92    High => "high",
93    Critical => "critical",
94});
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
97#[serde(rename_all = "snake_case")]
98pub enum IssueStatus {
99    Open,
100    Triaged,
101    NeedsInfo,
102    Resolved,
103    Closed,
104}
105
106issue_enum!(IssueStatus, "issue status", {
107    Open => "open",
108    Triaged => "triaged",
109    NeedsInfo => "needs_info",
110    Resolved => "resolved",
111    Closed => "closed",
112});
113
114impl IssueStatus {
115    #[must_use]
116    pub const fn is_active(self) -> bool {
117        matches!(self, Self::Open | Self::Triaged | Self::NeedsInfo)
118    }
119}
120
121/// The platform records a report points at, captured from the filing
122/// request rather than supplied by the reporter.
123#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
124pub struct IssueContext {
125    pub trace: Option<TraceId>,
126    pub context: Option<ContextId>,
127    pub session: Option<SessionId>,
128    pub task: Option<TaskId>,
129    pub mcp_execution: Option<McpExecutionId>,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
133pub struct IssueAttachment {
134    pub upload_id: DocumentUploadId,
135    pub content_sha256: ContentHash,
136    pub file_name: String,
137    pub mime_type: String,
138    pub size_bytes: i64,
139    pub created_at: DateTime<Utc>,
140}
141
142/// One report as its reporter and triage see it.
143#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
144pub struct IssueReport {
145    pub id: IssueReportId,
146    pub reporter_user_id: UserId,
147    pub via_agent: Option<String>,
148    pub kind: IssueKind,
149    pub severity: IssueSeverity,
150    pub status: IssueStatus,
151    pub title: String,
152    pub body: String,
153    pub occurrences: i32,
154    pub context: IssueContext,
155    // JSON: the reporter's client facts are a free-form, size-bounded object.
156    pub client: serde_json::Value,
157    pub triage_note: Option<String>,
158    pub triaged_by: Option<UserId>,
159    pub created_at: DateTime<Utc>,
160    pub last_seen_at: DateTime<Utc>,
161    pub updated_at: DateTime<Utc>,
162    pub closed_at: Option<DateTime<Utc>>,
163    #[serde(default)]
164    pub attachments: Vec<IssueAttachment>,
165}