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
26use schemars::JsonSchema;
27use serde::{Deserialize, Serialize};
28
29use super::{
30    AudioArtifact, ChartArtifact, CopyPasteTextArtifact, DashboardArtifact, ImageArtifact,
31    ListArtifact, MessageArtifact, PresentationCardArtifact, TableArtifact, TextArtifact,
32    VideoArtifact,
33};
34
35#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
36#[serde(tag = "artifact_type", rename_all = "snake_case")]
37pub enum CliArtifact {
38    Table {
39        #[serde(flatten)]
40        artifact: TableArtifact,
41    },
42    List {
43        #[serde(flatten)]
44        artifact: ListArtifact,
45    },
46    Text {
47        #[serde(flatten)]
48        artifact: TextArtifact,
49    },
50    #[serde(rename = "copy_paste_text")]
51    CopyPasteText {
52        #[serde(flatten)]
53        artifact: CopyPasteTextArtifact,
54    },
55    Dashboard {
56        #[serde(flatten)]
57        artifact: DashboardArtifact,
58    },
59    Chart {
60        #[serde(flatten)]
61        artifact: ChartArtifact,
62    },
63    Audio {
64        #[serde(flatten)]
65        artifact: AudioArtifact,
66    },
67    Image {
68        #[serde(flatten)]
69        artifact: ImageArtifact,
70    },
71    Video {
72        #[serde(flatten)]
73        artifact: VideoArtifact,
74    },
75    #[serde(rename = "presentation_card")]
76    PresentationCard {
77        #[serde(flatten)]
78        artifact: PresentationCardArtifact,
79    },
80    Message {
81        #[serde(flatten)]
82        artifact: MessageArtifact,
83    },
84}
85
86impl CliArtifact {
87    pub const ENVELOPE_TYPE_STR: &'static str = "cli";
88
89    #[must_use]
90    pub const fn artifact_type_str(&self) -> &'static str {
91        match self {
92            Self::Table { .. } => TableArtifact::ARTIFACT_TYPE_STR,
93            Self::List { .. } => ListArtifact::ARTIFACT_TYPE_STR,
94            Self::Text { .. } => TextArtifact::ARTIFACT_TYPE_STR,
95            Self::CopyPasteText { .. } => CopyPasteTextArtifact::ARTIFACT_TYPE_STR,
96            Self::Dashboard { .. } => DashboardArtifact::ARTIFACT_TYPE_STR,
97            Self::Chart { .. } => ChartArtifact::ARTIFACT_TYPE_STR,
98            Self::Audio { .. } => AudioArtifact::ARTIFACT_TYPE_STR,
99            Self::Image { .. } => ImageArtifact::ARTIFACT_TYPE_STR,
100            Self::Video { .. } => VideoArtifact::ARTIFACT_TYPE_STR,
101            Self::PresentationCard { .. } => PresentationCardArtifact::ARTIFACT_TYPE_STR,
102            Self::Message { .. } => MessageArtifact::ARTIFACT_TYPE_STR,
103        }
104    }
105
106    #[must_use]
107    pub fn title(&self) -> Option<String> {
108        match self {
109            Self::Text { artifact } => artifact.title.clone(),
110            Self::CopyPasteText { artifact } => artifact.title.clone(),
111            Self::Dashboard { artifact } => Some(artifact.title.clone()),
112            Self::Audio { artifact } => artifact.title.clone(),
113            Self::PresentationCard { artifact } => Some(artifact.title.clone()),
114            Self::Table { .. }
115            | Self::List { .. }
116            | Self::Chart { .. }
117            | Self::Image { .. }
118            | Self::Video { .. }
119            | Self::Message { .. } => None,
120        }
121    }
122
123    #[must_use]
124    pub const fn table(artifact: TableArtifact) -> Self {
125        Self::Table { artifact }
126    }
127
128    #[must_use]
129    pub const fn list(artifact: ListArtifact) -> Self {
130        Self::List { artifact }
131    }
132
133    #[must_use]
134    pub const fn text(artifact: TextArtifact) -> Self {
135        Self::Text { artifact }
136    }
137
138    #[must_use]
139    pub const fn copy_paste_text(artifact: CopyPasteTextArtifact) -> Self {
140        Self::CopyPasteText { artifact }
141    }
142
143    #[must_use]
144    pub const fn dashboard(artifact: DashboardArtifact) -> Self {
145        Self::Dashboard { artifact }
146    }
147
148    #[must_use]
149    pub const fn chart(artifact: ChartArtifact) -> Self {
150        Self::Chart { artifact }
151    }
152
153    #[must_use]
154    pub const fn audio(artifact: AudioArtifact) -> Self {
155        Self::Audio { artifact }
156    }
157
158    #[must_use]
159    pub const fn image(artifact: ImageArtifact) -> Self {
160        Self::Image { artifact }
161    }
162
163    #[must_use]
164    pub const fn video(artifact: VideoArtifact) -> Self {
165        Self::Video { artifact }
166    }
167
168    #[must_use]
169    pub const fn presentation_card(artifact: PresentationCardArtifact) -> Self {
170        Self::PresentationCard { artifact }
171    }
172
173    #[must_use]
174    pub const fn message(artifact: MessageArtifact) -> Self {
175        Self::Message { artifact }
176    }
177}