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, reading
23//! `x-artifact-type` first and `artifact_type` as the fallback — the former is
24//! the canonical per-artifact tag, the latter a serde implementation detail
25//! that only exists on enveloped payloads. Collapsing the two would either
26//! erase the union from the schema or mis-type every enveloped artifact, so
27//! both tags stay on the wire.
28//!
29//! Copyright (c) systemprompt.io — Business Source License 1.1.
30//! See <https://systemprompt.io> for licensing details.
31
32use schemars::JsonSchema;
33use serde::{Deserialize, Serialize};
34
35use super::{
36    AudioArtifact, ChartArtifact, CopyPasteTextArtifact, DashboardArtifact, ImageArtifact,
37    ListArtifact, MessageArtifact, PresentationCardArtifact, TableArtifact, TextArtifact,
38    VideoArtifact,
39};
40
41#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
42#[serde(tag = "artifact_type", rename_all = "snake_case")]
43pub enum CliArtifact {
44    Table {
45        #[serde(flatten)]
46        artifact: TableArtifact,
47    },
48    List {
49        #[serde(flatten)]
50        artifact: ListArtifact,
51    },
52    Text {
53        #[serde(flatten)]
54        artifact: TextArtifact,
55    },
56    #[serde(rename = "copy_paste_text")]
57    CopyPasteText {
58        #[serde(flatten)]
59        artifact: CopyPasteTextArtifact,
60    },
61    Dashboard {
62        #[serde(flatten)]
63        artifact: DashboardArtifact,
64    },
65    Chart {
66        #[serde(flatten)]
67        artifact: ChartArtifact,
68    },
69    Audio {
70        #[serde(flatten)]
71        artifact: AudioArtifact,
72    },
73    Image {
74        #[serde(flatten)]
75        artifact: ImageArtifact,
76    },
77    Video {
78        #[serde(flatten)]
79        artifact: VideoArtifact,
80    },
81    #[serde(rename = "presentation_card")]
82    PresentationCard {
83        #[serde(flatten)]
84        artifact: PresentationCardArtifact,
85    },
86    Message {
87        #[serde(flatten)]
88        artifact: MessageArtifact,
89    },
90}
91
92impl CliArtifact {
93    pub const ENVELOPE_TYPE_STR: &'static str = "cli";
94
95    #[must_use]
96    pub const fn artifact_type_str(&self) -> &'static str {
97        match self {
98            Self::Table { .. } => TableArtifact::ARTIFACT_TYPE_STR,
99            Self::List { .. } => ListArtifact::ARTIFACT_TYPE_STR,
100            Self::Text { .. } => TextArtifact::ARTIFACT_TYPE_STR,
101            Self::CopyPasteText { .. } => CopyPasteTextArtifact::ARTIFACT_TYPE_STR,
102            Self::Dashboard { .. } => DashboardArtifact::ARTIFACT_TYPE_STR,
103            Self::Chart { .. } => ChartArtifact::ARTIFACT_TYPE_STR,
104            Self::Audio { .. } => AudioArtifact::ARTIFACT_TYPE_STR,
105            Self::Image { .. } => ImageArtifact::ARTIFACT_TYPE_STR,
106            Self::Video { .. } => VideoArtifact::ARTIFACT_TYPE_STR,
107            Self::PresentationCard { .. } => PresentationCardArtifact::ARTIFACT_TYPE_STR,
108            Self::Message { .. } => MessageArtifact::ARTIFACT_TYPE_STR,
109        }
110    }
111
112    #[must_use]
113    pub fn title(&self) -> Option<String> {
114        match self {
115            Self::Text { artifact } => artifact.title.clone(),
116            Self::CopyPasteText { artifact } => artifact.title.clone(),
117            Self::Dashboard { artifact } => Some(artifact.title.clone()),
118            Self::Audio { artifact } => artifact.title.clone(),
119            Self::PresentationCard { artifact } => Some(artifact.title.clone()),
120            Self::Table { artifact } => artifact.title.clone(),
121            Self::Chart { artifact } => Some(artifact.title.clone()),
122            Self::List { .. } | Self::Image { .. } | Self::Video { .. } | Self::Message { .. } => {
123                None
124            },
125        }
126    }
127
128    #[must_use]
129    pub fn text_body(&self) -> Option<String> {
130        match self {
131            Self::Text { artifact } => Some(artifact.content.clone()),
132            Self::CopyPasteText { artifact } => Some(artifact.content.clone()),
133            _ => None,
134        }
135    }
136
137    #[must_use]
138    pub const fn table(artifact: TableArtifact) -> Self {
139        Self::Table { artifact }
140    }
141
142    #[must_use]
143    pub const fn list(artifact: ListArtifact) -> Self {
144        Self::List { artifact }
145    }
146
147    #[must_use]
148    pub const fn text(artifact: TextArtifact) -> Self {
149        Self::Text { artifact }
150    }
151
152    #[must_use]
153    pub const fn copy_paste_text(artifact: CopyPasteTextArtifact) -> Self {
154        Self::CopyPasteText { artifact }
155    }
156
157    #[must_use]
158    pub const fn dashboard(artifact: DashboardArtifact) -> Self {
159        Self::Dashboard { artifact }
160    }
161
162    #[must_use]
163    pub const fn chart(artifact: ChartArtifact) -> Self {
164        Self::Chart { artifact }
165    }
166
167    #[must_use]
168    pub const fn audio(artifact: AudioArtifact) -> Self {
169        Self::Audio { artifact }
170    }
171
172    #[must_use]
173    pub const fn image(artifact: ImageArtifact) -> Self {
174        Self::Image { artifact }
175    }
176
177    #[must_use]
178    pub const fn video(artifact: VideoArtifact) -> Self {
179        Self::Video { artifact }
180    }
181
182    #[must_use]
183    pub const fn presentation_card(artifact: PresentationCardArtifact) -> Self {
184        Self::PresentationCard { artifact }
185    }
186
187    #[must_use]
188    pub const fn message(artifact: MessageArtifact) -> Self {
189        Self::Message { artifact }
190    }
191}