Skip to main content

systemprompt_models/artifacts/tool_result/
mod.rs

1//! Tool-result artifact: the typed form of an MCP `CallToolResult` as the
2//! model saw it.
3//!
4//! Every tool result that reaches the platform — from an in-process server, a
5//! proxied external server, a gateway `tool_result` block, or a client hook —
6//! is normalised into a [`ToolResultArtifact`] so that one artifact model
7//! covers every source. The MCP result is itself a typed wire schema (content
8//! blocks, optional structured content, error flag), so mirroring it here is a
9//! typed model, not a JSON escape hatch: binary block bytes are never stored,
10//! only their length and digest.
11//!
12//! Copyright (c) systemprompt.io — Business Source License 1.1.
13//! See <https://systemprompt.io> for licensing details.
14
15use schemars::JsonSchema;
16use serde::{Deserialize, Serialize};
17use serde_json::{Value as JsonValue, json};
18
19use crate::artifacts::traits::Artifact;
20use crate::artifacts::types::ArtifactType;
21
22fn default_artifact_type() -> String {
23    ToolResultArtifact::ARTIFACT_TYPE_STR.to_owned()
24}
25
26/// One content block of a tool result, with binary payloads reduced to their
27/// size and digest.
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
29#[serde(tag = "type", rename_all = "snake_case")]
30pub enum ToolResultBlock {
31    Text {
32        text: String,
33    },
34    Image {
35        mime_type: String,
36        byte_len: u64,
37        sha256: String,
38    },
39    Audio {
40        mime_type: String,
41        byte_len: u64,
42        sha256: String,
43    },
44    Resource {
45        uri: String,
46        #[serde(default, skip_serializing_if = "Option::is_none")]
47        mime_type: Option<String>,
48        #[serde(default, skip_serializing_if = "Option::is_none")]
49        text: Option<String>,
50        #[serde(default, skip_serializing_if = "Option::is_none")]
51        blob_byte_len: Option<u64>,
52        #[serde(default, skip_serializing_if = "Option::is_none")]
53        blob_sha256: Option<String>,
54    },
55    ResourceLink {
56        uri: String,
57        name: String,
58        #[serde(default, skip_serializing_if = "Option::is_none")]
59        mime_type: Option<String>,
60    },
61}
62
63impl ToolResultBlock {
64    #[must_use]
65    pub fn is_ui_resource(&self) -> bool {
66        match self {
67            Self::Resource { uri, .. } | Self::ResourceLink { uri, .. } => uri.starts_with("ui://"),
68            Self::Text { .. } | Self::Image { .. } | Self::Audio { .. } => false,
69        }
70    }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
74pub struct ToolResultArtifact {
75    #[serde(rename = "x-artifact-type")]
76    #[serde(default = "default_artifact_type")]
77    pub artifact_type: String,
78    pub tool_name: String,
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub server_name: Option<String>,
81    #[serde(default)]
82    pub is_error: bool,
83    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
84    pub truncated: bool,
85    #[serde(default)]
86    pub blocks: Vec<ToolResultBlock>,
87    // JSON: MCP `structuredContent` is the tool's own output-schema object.
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub structured_content: Option<JsonValue>,
90}
91
92impl ToolResultArtifact {
93    pub const ARTIFACT_TYPE_STR: &'static str = "tool_result";
94
95    #[must_use]
96    pub fn new(tool_name: impl Into<String>) -> Self {
97        Self {
98            artifact_type: default_artifact_type(),
99            tool_name: tool_name.into(),
100            server_name: None,
101            is_error: false,
102            truncated: false,
103            blocks: Vec::new(),
104            structured_content: None,
105        }
106    }
107
108    #[must_use]
109    pub fn with_server(mut self, server_name: impl Into<String>) -> Self {
110        self.server_name = Some(server_name.into());
111        self
112    }
113
114    #[must_use]
115    pub const fn with_error(mut self, is_error: bool) -> Self {
116        self.is_error = is_error;
117        self
118    }
119
120    #[must_use]
121    pub fn with_blocks(mut self, blocks: Vec<ToolResultBlock>) -> Self {
122        self.blocks = blocks;
123        self
124    }
125
126    #[must_use]
127    pub fn with_structured_content(mut self, value: Option<JsonValue>) -> Self {
128        self.structured_content = value;
129        self
130    }
131
132    #[must_use]
133    pub fn has_ui_resource(&self) -> bool {
134        self.blocks.iter().any(ToolResultBlock::is_ui_resource)
135    }
136}
137
138impl Artifact for ToolResultArtifact {
139    fn artifact_type(&self) -> ArtifactType {
140        ArtifactType::ToolResult
141    }
142
143    // JSON: JSON Schema document describing the artifact for the model.
144    fn to_schema(&self) -> JsonValue {
145        json!({
146            "type": "object",
147            "properties": {
148                "tool_name": { "type": "string" },
149                "server_name": { "type": "string" },
150                "is_error": { "type": "boolean" },
151                "blocks": { "type": "array", "items": { "type": "object" } },
152                "structured_content": { "type": "object" }
153            },
154            "required": ["tool_name", "blocks"],
155            "x-artifact-type": Self::ARTIFACT_TYPE_STR
156        })
157    }
158}