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