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