Skip to main content

agent_client_protocol_schema/v2/
tool_call.rs

1//! Tool calls represent actions that language models request agents to perform.
2//!
3//! When an LLM determines it needs to interact with external systems—like reading files,
4//! running code, or fetching data—it generates tool calls that the agent executes on its behalf.
5//!
6/// See protocol docs: [Tool Calls](https://agentclientprotocol.com/protocol/tool-calls)
7use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
8
9use derive_more::{Display, From};
10use schemars::{JsonSchema, Schema};
11use serde::{Deserialize, Serialize};
12use serde_with::{DefaultOnError, VecSkipError, serde_as, skip_serializing_none};
13
14use super::{ContentBlock, Meta};
15use crate::{IntoMaybeUndefined, IntoOption, MaybeUndefined, SkipListener};
16
17/// Represents an upsert for a tool call that the language model has requested.
18///
19/// Tool calls are actions that the agent executes on behalf of the language model,
20/// such as reading files, executing code, or fetching data from external sources.
21///
22/// Only [`ToolCallUpdate::tool_call_id`] is required. Other fields have patch semantics:
23/// omitted fields leave the existing tool call value unchanged, `null` clears or
24/// unsets the value, and concrete values replace the previous value. For
25/// collection fields, concrete arrays replace the previous collection, and both
26/// `null` and `[]` clear the collection. When a client receives a tool call ID it
27/// has not seen before, omitted fields use client defaults.
28///
29/// See protocol docs: [Tool Calls](https://agentclientprotocol.com/protocol/tool-calls)
30#[serde_as]
31#[skip_serializing_none]
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
33#[serde(rename_all = "camelCase")]
34#[non_exhaustive]
35pub struct ToolCallUpdate {
36    /// Unique identifier for this tool call within the session.
37    pub tool_call_id: ToolCallId,
38    /// Human-readable title describing what the tool is doing.
39    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
40    pub title: MaybeUndefined<String>,
41    /// The category of tool being invoked.
42    /// Helps clients choose appropriate icons and UI treatment.
43    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
44    pub kind: MaybeUndefined<ToolKind>,
45    /// Current execution status of the tool call.
46    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
47    pub status: MaybeUndefined<ToolCallStatus>,
48    /// Content produced by the tool call.
49    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
50    #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
51    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
52    pub content: MaybeUndefined<Vec<ToolCallContent>>,
53    /// File locations affected by this tool call.
54    /// Enables "follow-along" features in clients.
55    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
56    #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
57    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
58    pub locations: MaybeUndefined<Vec<ToolCallLocation>>,
59    /// Raw input parameters sent to the tool.
60    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
61    pub raw_input: MaybeUndefined<serde_json::Value>,
62    /// Raw output returned by the tool.
63    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
64    pub raw_output: MaybeUndefined<serde_json::Value>,
65    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
66    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
67    /// these keys.
68    ///
69    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
70    #[serde(rename = "_meta")]
71    pub meta: Option<Meta>,
72}
73
74impl ToolCallUpdate {
75    #[must_use]
76    pub fn new(tool_call_id: impl Into<ToolCallId>) -> Self {
77        Self {
78            tool_call_id: tool_call_id.into(),
79            title: MaybeUndefined::Undefined,
80            kind: MaybeUndefined::Undefined,
81            status: MaybeUndefined::Undefined,
82            content: MaybeUndefined::Undefined,
83            locations: MaybeUndefined::Undefined,
84            raw_input: MaybeUndefined::Undefined,
85            raw_output: MaybeUndefined::Undefined,
86            meta: None,
87        }
88    }
89
90    /// Human-readable title describing what the tool is doing.
91    #[must_use]
92    pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
93        self.title = title.into_maybe_undefined();
94        self
95    }
96
97    /// The category of tool being invoked.
98    /// Helps clients choose appropriate icons and UI treatment.
99    #[must_use]
100    pub fn kind(mut self, kind: impl IntoMaybeUndefined<ToolKind>) -> Self {
101        self.kind = kind.into_maybe_undefined();
102        self
103    }
104
105    /// Current execution status of the tool call.
106    #[must_use]
107    pub fn status(mut self, status: impl IntoMaybeUndefined<ToolCallStatus>) -> Self {
108        self.status = status.into_maybe_undefined();
109        self
110    }
111
112    /// Content produced by the tool call.
113    #[must_use]
114    pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ToolCallContent>>) -> Self {
115        self.content = content.into_maybe_undefined();
116        self
117    }
118
119    /// File locations affected by this tool call.
120    /// Enables "follow-along" features in clients.
121    #[must_use]
122    pub fn locations(mut self, locations: impl IntoMaybeUndefined<Vec<ToolCallLocation>>) -> Self {
123        self.locations = locations.into_maybe_undefined();
124        self
125    }
126
127    /// Raw input parameters sent to the tool.
128    #[must_use]
129    pub fn raw_input(mut self, raw_input: impl IntoMaybeUndefined<serde_json::Value>) -> Self {
130        self.raw_input = raw_input.into_maybe_undefined();
131        self
132    }
133
134    /// Raw output returned by the tool.
135    #[must_use]
136    pub fn raw_output(mut self, raw_output: impl IntoMaybeUndefined<serde_json::Value>) -> Self {
137        self.raw_output = raw_output.into_maybe_undefined();
138        self
139    }
140
141    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
142    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
143    /// these keys.
144    ///
145    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
146    #[must_use]
147    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
148        self.meta = meta.into_option();
149        self
150    }
151
152    /// Applies a later tool-call patch to this stored tool-call state.
153    ///
154    /// Fields set to `null` are preserved as `null` so callers can decide how to
155    /// render an explicitly cleared value.
156    pub fn apply_update(&mut self, update: ToolCallUpdate) {
157        debug_assert_eq!(self.tool_call_id, update.tool_call_id);
158        if !update.title.is_undefined() {
159            self.title = update.title;
160        }
161        if !update.kind.is_undefined() {
162            self.kind = update.kind;
163        }
164        if !update.status.is_undefined() {
165            self.status = update.status;
166        }
167        if !update.content.is_undefined() {
168            self.content = update.content;
169        }
170        if !update.locations.is_undefined() {
171            self.locations = update.locations;
172        }
173        if !update.raw_input.is_undefined() {
174            self.raw_input = update.raw_input;
175        }
176        if !update.raw_output.is_undefined() {
177            self.raw_output = update.raw_output;
178        }
179    }
180}
181
182/// A streamed item of tool-call content.
183///
184/// Tool-call content chunks append one [`ToolCallContent`] item to the current
185/// content for the matching [`ToolCallId`]. Agents can use
186/// [`ToolCallUpdate::content`] when they need to replace the whole content
187/// collection instead.
188#[skip_serializing_none]
189#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
190#[serde(rename_all = "camelCase")]
191#[non_exhaustive]
192pub struct ToolCallContentChunk {
193    /// The ID of the tool call this content belongs to.
194    pub tool_call_id: ToolCallId,
195    /// A single item of content produced by the tool call.
196    pub content: ToolCallContent,
197    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
198    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
199    /// these keys. This field is optional; omitted or `null` means there is no
200    /// chunk-level metadata.
201    ///
202    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
203    #[serde(rename = "_meta")]
204    pub meta: Option<Meta>,
205}
206
207impl ToolCallContentChunk {
208    #[must_use]
209    pub fn new(tool_call_id: impl Into<ToolCallId>, content: impl Into<ToolCallContent>) -> Self {
210        Self {
211            tool_call_id: tool_call_id.into(),
212            content: content.into(),
213            meta: None,
214        }
215    }
216
217    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
218    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
219    /// these keys.
220    ///
221    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
222    #[must_use]
223    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
224        self.meta = meta.into_option();
225        self
226    }
227}
228
229/// Unique identifier for a tool call within a session.
230#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash, Display, From)]
231#[serde(transparent)]
232#[from(Arc<str>, String, &'static str)]
233#[non_exhaustive]
234pub struct ToolCallId(pub Arc<str>);
235
236impl ToolCallId {
237    #[must_use]
238    pub fn new(id: impl Into<Arc<str>>) -> Self {
239        Self(id.into())
240    }
241}
242
243impl IntoOption<ToolCallId> for &str {
244    fn into_option(self) -> Option<ToolCallId> {
245        Some(ToolCallId::new(self))
246    }
247}
248
249/// Categories of tools that can be invoked.
250///
251/// Tool kinds help clients choose appropriate icons and optimize how they
252/// display tool execution progress.
253///
254/// See protocol docs: [Creating](https://agentclientprotocol.com/protocol/tool-calls#creating)
255#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
256#[serde(rename_all = "snake_case")]
257#[non_exhaustive]
258pub enum ToolKind {
259    /// Reading files or data.
260    Read,
261    /// Modifying files or content.
262    Edit,
263    /// Removing files or data.
264    Delete,
265    /// Moving or renaming files.
266    Move,
267    /// Searching for information.
268    Search,
269    /// Running commands or code.
270    Execute,
271    /// Internal reasoning or planning.
272    Think,
273    /// Retrieving external data.
274    Fetch,
275    /// Switching the current session mode.
276    SwitchMode,
277    /// Other tool types (default).
278    #[default]
279    Other,
280    /// Custom or future tool kind.
281    ///
282    /// Values beginning with `_` are reserved for implementation-specific
283    /// extensions. Unknown values that do not begin with `_` are reserved for
284    /// future ACP variants.
285    #[serde(untagged)]
286    Unknown(String),
287}
288
289/// Execution status of a tool call.
290///
291/// Tool calls progress through different statuses during their lifecycle.
292///
293/// See protocol docs: [Status](https://agentclientprotocol.com/protocol/tool-calls#status)
294#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
295#[serde(rename_all = "snake_case")]
296#[non_exhaustive]
297pub enum ToolCallStatus {
298    /// The tool call hasn't started running yet because the input is either
299    /// streaming or we're awaiting approval.
300    #[default]
301    Pending,
302    /// The tool call is currently running.
303    InProgress,
304    /// The tool call completed successfully.
305    Completed,
306    /// The tool call failed with an error.
307    Failed,
308    /// Custom or future tool call status.
309    ///
310    /// Values beginning with `_` are reserved for implementation-specific
311    /// extensions. Unknown values that do not begin with `_` are reserved for
312    /// future ACP variants.
313    #[serde(untagged)]
314    Other(String),
315}
316
317/// Content produced by a tool call.
318///
319/// Tool calls can produce different types of content including
320/// standard content blocks (text, images) or file diffs.
321///
322/// See protocol docs: [Content](https://agentclientprotocol.com/protocol/tool-calls#content)
323#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
324#[serde(tag = "type", rename_all = "snake_case")]
325#[schemars(extend("discriminator" = {"propertyName": "type"}))]
326#[non_exhaustive]
327pub enum ToolCallContent {
328    /// Standard content block (text, images, resources).
329    Content(Box<Content>),
330    /// File modification shown as a diff.
331    Diff(Diff),
332    /// Custom or future tool call content.
333    ///
334    /// Values beginning with `_` are reserved for implementation-specific
335    /// extensions. Unknown values that do not begin with `_` are reserved for
336    /// future ACP variants.
337    ///
338    /// Receivers that do not understand this content type should preserve the
339    /// raw payload when storing, replaying, proxying, or forwarding tool call
340    /// output, and otherwise ignore it or display it generically.
341    #[serde(untagged)]
342    Other(OtherToolCallContent),
343}
344
345/// Custom or future tool call content payload.
346#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
347#[schemars(inline)]
348#[schemars(transform = other_tool_call_content_schema)]
349#[serde(rename_all = "camelCase")]
350#[non_exhaustive]
351pub struct OtherToolCallContent {
352    /// Custom or future tool call content type.
353    ///
354    /// Values beginning with `_` are reserved for implementation-specific
355    /// extensions. Unknown values that do not begin with `_` are reserved for
356    /// future ACP variants.
357    #[serde(rename = "type")]
358    pub type_: String,
359    /// Additional fields from the unknown tool call content payload.
360    #[serde(flatten)]
361    pub fields: BTreeMap<String, serde_json::Value>,
362}
363
364impl OtherToolCallContent {
365    #[must_use]
366    pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
367        fields.remove("type");
368        Self {
369            type_: type_.into(),
370            fields,
371        }
372    }
373}
374
375impl<'de> Deserialize<'de> for OtherToolCallContent {
376    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
377    where
378        D: serde::Deserializer<'de>,
379    {
380        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
381        let type_ = fields
382            .remove("type")
383            .ok_or_else(|| serde::de::Error::missing_field("type"))?;
384        let serde_json::Value::String(type_) = type_ else {
385            return Err(serde::de::Error::custom("`type` must be a string"));
386        };
387
388        if is_known_tool_call_content_type(&type_) {
389            return Err(serde::de::Error::custom(format!(
390                "known tool call content `{type_}` did not match its schema"
391            )));
392        }
393
394        Ok(Self { type_, fields })
395    }
396}
397
398fn is_known_tool_call_content_type(type_: &str) -> bool {
399    matches!(type_, "content" | "diff")
400}
401
402fn other_tool_call_content_schema(schema: &mut Schema) {
403    super::schema_util::reject_known_string_discriminators(schema, "type", &["content", "diff"]);
404}
405
406impl<T: Into<ContentBlock>> From<T> for ToolCallContent {
407    fn from(content: T) -> Self {
408        ToolCallContent::Content(Box::new(Content::new(content)))
409    }
410}
411
412impl From<Diff> for ToolCallContent {
413    fn from(diff: Diff) -> Self {
414        ToolCallContent::Diff(diff)
415    }
416}
417
418/// Standard content block (text, images, resources).
419#[skip_serializing_none]
420#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
421#[serde(rename_all = "camelCase")]
422#[non_exhaustive]
423pub struct Content {
424    /// The actual content block.
425    pub content: ContentBlock,
426    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
427    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
428    /// these keys.
429    ///
430    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
431    #[serde(rename = "_meta")]
432    pub meta: Option<Meta>,
433}
434
435impl Content {
436    #[must_use]
437    pub fn new(content: impl Into<ContentBlock>) -> Self {
438        Self {
439            content: content.into(),
440            meta: None,
441        }
442    }
443
444    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
445    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
446    /// these keys.
447    ///
448    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
449    #[must_use]
450    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
451        self.meta = meta.into_option();
452        self
453    }
454}
455
456/// A diff representing file modifications.
457///
458/// Shows changes to files in a format suitable for display in the client UI.
459///
460/// See protocol docs: [Content](https://agentclientprotocol.com/protocol/tool-calls#content)
461#[skip_serializing_none]
462#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
463#[serde(rename_all = "camelCase")]
464#[non_exhaustive]
465pub struct Diff {
466    /// The file path being modified.
467    pub path: PathBuf,
468    /// The original content (None for new files).
469    pub old_text: Option<String>,
470    /// The new content after modification.
471    pub new_text: String,
472    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
473    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
474    /// these keys.
475    ///
476    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
477    #[serde(rename = "_meta")]
478    pub meta: Option<Meta>,
479}
480
481impl Diff {
482    #[must_use]
483    pub fn new(path: impl Into<PathBuf>, new_text: impl Into<String>) -> Self {
484        Self {
485            path: path.into(),
486            old_text: None,
487            new_text: new_text.into(),
488            meta: None,
489        }
490    }
491
492    /// The original content (None for new files).
493    #[must_use]
494    pub fn old_text(mut self, old_text: impl IntoOption<String>) -> Self {
495        self.old_text = old_text.into_option();
496        self
497    }
498
499    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
500    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
501    /// these keys.
502    ///
503    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
504    #[must_use]
505    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
506        self.meta = meta.into_option();
507        self
508    }
509}
510
511/// A file location being accessed or modified by a tool.
512///
513/// Enables clients to implement "follow-along" features that track
514/// which files the agent is working with in real-time.
515///
516/// See protocol docs: [Following the Agent](https://agentclientprotocol.com/protocol/tool-calls#following-the-agent)
517#[skip_serializing_none]
518#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
519#[serde(rename_all = "camelCase")]
520#[non_exhaustive]
521pub struct ToolCallLocation {
522    /// The file path being accessed or modified.
523    pub path: PathBuf,
524    /// Optional line number within the file.
525    #[serde(default)]
526    pub line: Option<u32>,
527    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
528    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
529    /// these keys.
530    ///
531    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
532    #[serde(rename = "_meta")]
533    pub meta: Option<Meta>,
534}
535
536impl ToolCallLocation {
537    #[must_use]
538    pub fn new(path: impl Into<PathBuf>) -> Self {
539        Self {
540            path: path.into(),
541            line: None,
542            meta: None,
543        }
544    }
545
546    /// Optional line number within the file.
547    #[must_use]
548    pub fn line(mut self, line: impl IntoOption<u32>) -> Self {
549        self.line = line.into_option();
550        self
551    }
552
553    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
554    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
555    /// these keys.
556    ///
557    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
558    #[must_use]
559    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
560        self.meta = meta.into_option();
561        self
562    }
563}
564
565#[cfg(test)]
566mod tests {
567    use super::*;
568    use crate::MaybeUndefined;
569
570    #[test]
571    fn tool_call_serializes_as_upsert() {
572        let tool_call = ToolCallUpdate::new("tc_1")
573            .title("Reading configuration")
574            .status(ToolCallStatus::InProgress)
575            .raw_input(serde_json::json!({"path": "settings.json"}));
576
577        assert_eq!(
578            serde_json::to_value(tool_call).unwrap(),
579            serde_json::json!({
580                "toolCallId": "tc_1",
581                "title": "Reading configuration",
582                "status": "in_progress",
583                "rawInput": {
584                    "path": "settings.json"
585                }
586            })
587        );
588    }
589
590    #[test]
591    fn tool_call_update_distinguishes_omitted_null_and_value() {
592        let tool_call = ToolCallUpdate::new("tc_1")
593            .status(ToolCallStatus::Completed)
594            .content(None::<Vec<ToolCallContent>>);
595
596        assert_eq!(
597            serde_json::to_value(tool_call).unwrap(),
598            serde_json::json!({
599                "toolCallId": "tc_1",
600                "status": "completed",
601                "content": null
602            })
603        );
604
605        let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
606            "toolCallId": "tc_1",
607            "status": null,
608            "locations": []
609        }))
610        .unwrap();
611        assert_eq!(deserialized.title, MaybeUndefined::Undefined);
612        assert_eq!(deserialized.status, MaybeUndefined::Null);
613        assert_eq!(deserialized.locations, MaybeUndefined::Value(Vec::new()));
614    }
615
616    #[test]
617    fn tool_call_update_skips_malformed_list_items() {
618        let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
619            "toolCallId": "tc_1",
620            "content": [
621                {
622                    "type": "content",
623                    "content": {
624                        "type": "text",
625                        "text": "ok"
626                    }
627                },
628                {
629                    "type": "diff",
630                    "path": "/bad"
631                }
632            ],
633            "locations": [
634                {
635                    "path": "/ok",
636                    "line": 3
637                },
638                {
639                    "line": 4
640                }
641            ]
642        }))
643        .unwrap();
644
645        let MaybeUndefined::Value(content) = deserialized.content else {
646            panic!("content should deserialize to a value");
647        };
648        assert_eq!(content.len(), 1);
649
650        let MaybeUndefined::Value(locations) = deserialized.locations else {
651            panic!("locations should deserialize to a value");
652        };
653        assert_eq!(locations.len(), 1);
654    }
655
656    #[test]
657    fn tool_call_content_chunk_serializes_single_content_item() {
658        let chunk = ToolCallContentChunk::new(
659            "tc_1",
660            ContentBlock::Text(crate::v2::TextContent::new("partial output")),
661        );
662
663        assert_eq!(
664            serde_json::to_value(chunk).unwrap(),
665            serde_json::json!({
666                "toolCallId": "tc_1",
667                "content": {
668                    "type": "content",
669                    "content": {
670                        "type": "text",
671                        "text": "partial output"
672                    }
673                }
674            })
675        );
676    }
677
678    #[test]
679    fn tool_kind_preserves_unknown_variant() {
680        let kind: ToolKind = serde_json::from_str("\"review\"").unwrap();
681        assert_eq!(kind, ToolKind::Unknown("review".to_string()));
682        assert_eq!(serde_json::to_value(&kind).unwrap(), "review");
683    }
684
685    #[test]
686    fn tool_call_status_preserves_unknown_variant() {
687        let status: ToolCallStatus = serde_json::from_str("\"deferred\"").unwrap();
688        assert_eq!(status, ToolCallStatus::Other("deferred".to_string()));
689        assert_eq!(serde_json::to_value(&status).unwrap(), "deferred");
690    }
691
692    #[test]
693    fn tool_call_content_preserves_unknown_variant() {
694        let content: ToolCallContent = serde_json::from_value(serde_json::json!({
695            "type": "_chart",
696            "title": "Tests",
697            "data": [1, 2, 3]
698        }))
699        .unwrap();
700
701        let ToolCallContent::Other(unknown) = content else {
702            panic!("expected unknown tool call content");
703        };
704
705        assert_eq!(unknown.type_, "_chart");
706        assert_eq!(
707            unknown.fields.get("title"),
708            Some(&serde_json::json!("Tests"))
709        );
710        assert_eq!(
711            serde_json::to_value(ToolCallContent::Other(unknown)).unwrap(),
712            serde_json::json!({
713                "type": "_chart",
714                "title": "Tests",
715                "data": [1, 2, 3]
716            })
717        );
718    }
719
720    #[test]
721    fn tool_call_content_does_not_hide_malformed_known_variant() {
722        assert!(
723            serde_json::from_value::<ToolCallContent>(serde_json::json!({
724                "type": "diff"
725            }))
726            .is_err()
727        );
728    }
729}