Skip to main content

systemprompt_models/artifacts/cli/
mod.rs

1//! CLI artifact envelope.
2//!
3//! [`CliArtifact`] is the tagged union of every renderable artifact a CLI
4//! command can emit (table, list, text, dashboard, chart, media, card,
5//! message). The CLI builds it, the wire carries it, and the MCP server
6//! deserializes it verbatim — the `artifact_type` tag is intrinsic to the
7//! serde representation.
8//!
9//! # Wire contract
10//!
11//! Two distinct tags travel with an enveloped artifact, and they are
12//! deliberately NOT unified:
13//!
14//! - The **envelope tag** [`CliArtifact::ENVELOPE_TYPE_STR`] (`"cli"`) is
15//!   advertised in tool output schemas (the top-level `x-artifact-type`). It
16//!   says "this output is a `CliArtifact` union", never which variant.
17//! - The **variant tag** is embedded in the serialized data itself: the
18//!   `artifact_type` serde tag (e.g. `"table"`), mirrored by the inner
19//!   artifact's `x-artifact-type` field.
20//!
21//! Schema consumers route on the envelope tag; renderers and type inference
22//! must fall through it to the data-embedded variant tag. Collapsing the two
23//! would either erase the union from the schema or mis-type every enveloped
24//! artifact, so both tags stay on the wire.
25//!
26//! Copyright (c) systemprompt.io — Business Source License 1.1.
27//! See <https://systemprompt.io> for licensing details.
28
29use schemars::JsonSchema;
30use serde::{Deserialize, Serialize};
31
32use super::{
33    AudioArtifact, ChartArtifact, CopyPasteTextArtifact, DashboardArtifact, ImageArtifact,
34    ListArtifact, MessageArtifact, PresentationCardArtifact, TableArtifact, TextArtifact,
35    VideoArtifact,
36};
37
38#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
39#[serde(tag = "artifact_type", rename_all = "snake_case")]
40pub enum CliArtifact {
41    Table {
42        #[serde(flatten)]
43        artifact: TableArtifact,
44    },
45    List {
46        #[serde(flatten)]
47        artifact: ListArtifact,
48    },
49    Text {
50        #[serde(flatten)]
51        artifact: TextArtifact,
52    },
53    #[serde(rename = "copy_paste_text")]
54    CopyPasteText {
55        #[serde(flatten)]
56        artifact: CopyPasteTextArtifact,
57    },
58    Dashboard {
59        #[serde(flatten)]
60        artifact: DashboardArtifact,
61    },
62    Chart {
63        #[serde(flatten)]
64        artifact: ChartArtifact,
65    },
66    Audio {
67        #[serde(flatten)]
68        artifact: AudioArtifact,
69    },
70    Image {
71        #[serde(flatten)]
72        artifact: ImageArtifact,
73    },
74    Video {
75        #[serde(flatten)]
76        artifact: VideoArtifact,
77    },
78    #[serde(rename = "presentation_card")]
79    PresentationCard {
80        #[serde(flatten)]
81        artifact: PresentationCardArtifact,
82    },
83    Message {
84        #[serde(flatten)]
85        artifact: MessageArtifact,
86    },
87}
88
89impl CliArtifact {
90    pub const ENVELOPE_TYPE_STR: &'static str = "cli";
91
92    #[must_use]
93    pub const fn artifact_type_str(&self) -> &'static str {
94        match self {
95            Self::Table { .. } => TableArtifact::ARTIFACT_TYPE_STR,
96            Self::List { .. } => ListArtifact::ARTIFACT_TYPE_STR,
97            Self::Text { .. } => TextArtifact::ARTIFACT_TYPE_STR,
98            Self::CopyPasteText { .. } => CopyPasteTextArtifact::ARTIFACT_TYPE_STR,
99            Self::Dashboard { .. } => DashboardArtifact::ARTIFACT_TYPE_STR,
100            Self::Chart { .. } => ChartArtifact::ARTIFACT_TYPE_STR,
101            Self::Audio { .. } => AudioArtifact::ARTIFACT_TYPE_STR,
102            Self::Image { .. } => ImageArtifact::ARTIFACT_TYPE_STR,
103            Self::Video { .. } => VideoArtifact::ARTIFACT_TYPE_STR,
104            Self::PresentationCard { .. } => PresentationCardArtifact::ARTIFACT_TYPE_STR,
105            Self::Message { .. } => MessageArtifact::ARTIFACT_TYPE_STR,
106        }
107    }
108
109    #[must_use]
110    pub fn title(&self) -> Option<String> {
111        match self {
112            Self::Text { artifact } => artifact.title.clone(),
113            Self::CopyPasteText { artifact } => artifact.title.clone(),
114            Self::Dashboard { artifact } => Some(artifact.title.clone()),
115            Self::Audio { artifact } => artifact.title.clone(),
116            Self::PresentationCard { artifact } => Some(artifact.title.clone()),
117            Self::Table { .. }
118            | Self::List { .. }
119            | Self::Chart { .. }
120            | Self::Image { .. }
121            | Self::Video { .. }
122            | Self::Message { .. } => None,
123        }
124    }
125
126    #[must_use]
127    pub const fn table(artifact: TableArtifact) -> Self {
128        Self::Table { artifact }
129    }
130
131    #[must_use]
132    pub const fn list(artifact: ListArtifact) -> Self {
133        Self::List { artifact }
134    }
135
136    #[must_use]
137    pub const fn text(artifact: TextArtifact) -> Self {
138        Self::Text { artifact }
139    }
140
141    #[must_use]
142    pub const fn copy_paste_text(artifact: CopyPasteTextArtifact) -> Self {
143        Self::CopyPasteText { artifact }
144    }
145
146    #[must_use]
147    pub const fn dashboard(artifact: DashboardArtifact) -> Self {
148        Self::Dashboard { artifact }
149    }
150
151    #[must_use]
152    pub const fn chart(artifact: ChartArtifact) -> Self {
153        Self::Chart { artifact }
154    }
155
156    #[must_use]
157    pub const fn audio(artifact: AudioArtifact) -> Self {
158        Self::Audio { artifact }
159    }
160
161    #[must_use]
162    pub const fn image(artifact: ImageArtifact) -> Self {
163        Self::Image { artifact }
164    }
165
166    #[must_use]
167    pub const fn video(artifact: VideoArtifact) -> Self {
168        Self::Video { artifact }
169    }
170
171    #[must_use]
172    pub const fn presentation_card(artifact: PresentationCardArtifact) -> Self {
173        Self::PresentationCard { artifact }
174    }
175
176    #[must_use]
177    pub const fn message(artifact: MessageArtifact) -> Self {
178        Self::Message { artifact }
179    }
180}