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, 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::{AbsolutePath, ContentBlock, MediaType, Meta, Terminal};
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    /// **UNSTABLE**
39    ///
40    /// This capability is not part of the spec yet, and may be removed or changed at any point.
41    ///
42    /// Programmatic name of the tool being invoked.
43    ///
44    /// This field is optional and has patch semantics. Omission means no
45    /// change, `null` clears the name, and a string replaces it. For a tool
46    /// call ID the client has not seen before, omission or `null` means that no
47    /// tool name is available.
48    #[cfg(feature = "unstable_tool_call_name")]
49    #[serde_as(deserialize_as = "DefaultOnError")]
50    #[schemars(extend("x-deserialize-default-on-error" = true))]
51    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
52    pub name: MaybeUndefined<String>,
53    /// Human-readable title describing what the tool is doing.
54    #[serde_as(deserialize_as = "DefaultOnError")]
55    #[schemars(extend("x-deserialize-default-on-error" = true))]
56    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
57    pub title: MaybeUndefined<String>,
58    /// The category of tool being invoked.
59    /// Helps clients choose appropriate icons and UI treatment.
60    #[serde_as(deserialize_as = "DefaultOnError")]
61    #[schemars(extend("x-deserialize-default-on-error" = true))]
62    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
63    pub kind: MaybeUndefined<ToolKind>,
64    /// Current execution status of the tool call.
65    #[serde_as(deserialize_as = "DefaultOnError")]
66    #[schemars(extend("x-deserialize-default-on-error" = true))]
67    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
68    pub status: MaybeUndefined<ToolCallStatus>,
69    /// Content produced by the tool call.
70    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
71    #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
72    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
73    pub content: MaybeUndefined<Vec<ToolCallContent>>,
74    /// File locations affected by this tool call.
75    /// Enables "follow-along" features in clients.
76    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<VecSkipError<_, SkipListener>>>")]
77    #[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
78    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
79    pub locations: MaybeUndefined<Vec<ToolCallLocation>>,
80    /// Raw input parameters sent to the tool.
81    #[serde_as(deserialize_as = "DefaultOnError")]
82    #[schemars(extend("x-deserialize-default-on-error" = true))]
83    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
84    pub raw_input: MaybeUndefined<serde_json::Value>,
85    /// Raw output returned by the tool.
86    #[serde_as(deserialize_as = "DefaultOnError")]
87    #[schemars(extend("x-deserialize-default-on-error" = true))]
88    #[serde(default, skip_serializing_if = "MaybeUndefined::is_undefined")]
89    pub raw_output: MaybeUndefined<serde_json::Value>,
90    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
91    /// metadata to their interactions. Omitted means no metadata update; `null` is an
92    /// explicit clear signal. Implementations MUST NOT make assumptions about values at these keys.
93    ///
94    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
95    #[serde_as(deserialize_as = "DefaultOnError<MaybeUndefined<_>>")]
96    #[schemars(extend("x-deserialize-default-on-error" = true))]
97    #[serde(
98        rename = "_meta",
99        default,
100        skip_serializing_if = "MaybeUndefined::is_undefined"
101    )]
102    pub meta: MaybeUndefined<Meta>,
103}
104
105impl ToolCallUpdate {
106    /// Builds [`ToolCallUpdate`] with the required fields set; optional fields start unset or empty.
107    #[must_use]
108    pub fn new(tool_call_id: impl Into<ToolCallId>) -> Self {
109        Self {
110            tool_call_id: tool_call_id.into(),
111            #[cfg(feature = "unstable_tool_call_name")]
112            name: MaybeUndefined::Undefined,
113            title: MaybeUndefined::Undefined,
114            kind: MaybeUndefined::Undefined,
115            status: MaybeUndefined::Undefined,
116            content: MaybeUndefined::Undefined,
117            locations: MaybeUndefined::Undefined,
118            raw_input: MaybeUndefined::Undefined,
119            raw_output: MaybeUndefined::Undefined,
120            meta: MaybeUndefined::Undefined,
121        }
122    }
123
124    /// **UNSTABLE**
125    ///
126    /// This capability is not part of the spec yet, and may be removed or changed at any point.
127    ///
128    /// Programmatic name of the tool being invoked.
129    #[cfg(feature = "unstable_tool_call_name")]
130    #[must_use]
131    pub fn name(mut self, name: impl IntoMaybeUndefined<String>) -> Self {
132        self.name = name.into_maybe_undefined();
133        self
134    }
135
136    /// Human-readable title describing what the tool is doing.
137    #[must_use]
138    pub fn title(mut self, title: impl IntoMaybeUndefined<String>) -> Self {
139        self.title = title.into_maybe_undefined();
140        self
141    }
142
143    /// The category of tool being invoked.
144    /// Helps clients choose appropriate icons and UI treatment.
145    #[must_use]
146    pub fn kind(mut self, kind: impl IntoMaybeUndefined<ToolKind>) -> Self {
147        self.kind = kind.into_maybe_undefined();
148        self
149    }
150
151    /// Current execution status of the tool call.
152    #[must_use]
153    pub fn status(mut self, status: impl IntoMaybeUndefined<ToolCallStatus>) -> Self {
154        self.status = status.into_maybe_undefined();
155        self
156    }
157
158    /// Content produced by the tool call.
159    #[must_use]
160    pub fn content(mut self, content: impl IntoMaybeUndefined<Vec<ToolCallContent>>) -> Self {
161        self.content = content.into_maybe_undefined();
162        self
163    }
164
165    /// File locations affected by this tool call.
166    /// Enables "follow-along" features in clients.
167    #[must_use]
168    pub fn locations(mut self, locations: impl IntoMaybeUndefined<Vec<ToolCallLocation>>) -> Self {
169        self.locations = locations.into_maybe_undefined();
170        self
171    }
172
173    /// Raw input parameters sent to the tool.
174    #[must_use]
175    pub fn raw_input(mut self, raw_input: impl IntoMaybeUndefined<serde_json::Value>) -> Self {
176        self.raw_input = raw_input.into_maybe_undefined();
177        self
178    }
179
180    /// Raw output returned by the tool.
181    #[must_use]
182    pub fn raw_output(mut self, raw_output: impl IntoMaybeUndefined<serde_json::Value>) -> Self {
183        self.raw_output = raw_output.into_maybe_undefined();
184        self
185    }
186
187    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
188    /// metadata to their interactions. Omitted means no metadata update; `null` is an
189    /// explicit clear signal. Implementations MUST NOT make assumptions about values at these keys.
190    ///
191    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
192    #[must_use]
193    pub fn meta(mut self, meta: impl IntoMaybeUndefined<Meta>) -> Self {
194        self.meta = meta.into_maybe_undefined();
195        self
196    }
197
198    /// Applies a later tool-call patch to this stored tool-call state.
199    ///
200    /// Fields set to `null` are preserved as `null` so callers can decide how to
201    /// render an explicitly cleared value.
202    pub fn apply_update(&mut self, update: ToolCallUpdate) {
203        debug_assert_eq!(self.tool_call_id, update.tool_call_id);
204        #[cfg(feature = "unstable_tool_call_name")]
205        if !update.name.is_undefined() {
206            self.name = update.name;
207        }
208        if !update.title.is_undefined() {
209            self.title = update.title;
210        }
211        if !update.kind.is_undefined() {
212            self.kind = update.kind;
213        }
214        if !update.status.is_undefined() {
215            self.status = update.status;
216        }
217        if !update.content.is_undefined() {
218            self.content = update.content;
219        }
220        if !update.locations.is_undefined() {
221            self.locations = update.locations;
222        }
223        if !update.raw_input.is_undefined() {
224            self.raw_input = update.raw_input;
225        }
226        if !update.raw_output.is_undefined() {
227            self.raw_output = update.raw_output;
228        }
229        if !update.meta.is_undefined() {
230            self.meta = update.meta;
231        }
232    }
233}
234
235/// A streamed item of tool-call content.
236///
237/// Tool-call content chunks append one [`ToolCallContent`] item to the current
238/// content for the matching [`ToolCallId`]. Agents can use
239/// [`ToolCallUpdate::content`] when they need to replace the whole content
240/// collection instead.
241#[serde_as]
242#[skip_serializing_none]
243#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
244#[serde(rename_all = "camelCase")]
245#[non_exhaustive]
246pub struct ToolCallContentChunk {
247    /// The ID of the tool call this content belongs to.
248    pub tool_call_id: ToolCallId,
249    /// A single item of content produced by the tool call.
250    pub content: ToolCallContent,
251    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
252    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
253    /// these keys. This field is chunk-scoped.
254    ///
255    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
256    #[serde_as(deserialize_as = "DefaultOnError")]
257    #[schemars(extend("x-deserialize-default-on-error" = true))]
258    #[serde(default)]
259    #[serde(rename = "_meta")]
260    pub meta: Option<Meta>,
261}
262
263impl ToolCallContentChunk {
264    /// Builds [`ToolCallContentChunk`] with the required fields set; optional fields start unset or empty.
265    #[must_use]
266    pub fn new(tool_call_id: impl Into<ToolCallId>, content: impl Into<ToolCallContent>) -> Self {
267        Self {
268            tool_call_id: tool_call_id.into(),
269            content: content.into(),
270            meta: None,
271        }
272    }
273
274    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
275    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
276    /// these keys. This field is chunk-scoped.
277    ///
278    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
279    #[must_use]
280    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
281        self.meta = meta.into_option();
282        self
283    }
284}
285
286/// Unique identifier for a tool call within a session.
287#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash, Display, From)]
288#[serde(transparent)]
289#[from(forward)]
290#[non_exhaustive]
291pub struct ToolCallId(pub Arc<str>);
292
293impl ToolCallId {
294    /// Wraps a protocol string as a typed [`ToolCallId`].
295    #[must_use]
296    pub fn new(id: impl Into<Self>) -> Self {
297        id.into()
298    }
299}
300
301impl IntoOption<ToolCallId> for &str {
302    fn into_option(self) -> Option<ToolCallId> {
303        Some(ToolCallId::new(self))
304    }
305}
306
307/// Categories of tools that can be invoked.
308///
309/// Tool kinds help clients choose appropriate icons and optimize how they
310/// display tool execution progress.
311///
312/// See protocol docs: [Creating](https://agentclientprotocol.com/protocol/tool-calls#creating)
313#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
314#[serde(rename_all = "snake_case")]
315#[non_exhaustive]
316pub enum ToolKind {
317    /// Reading files or data.
318    Read,
319    /// Modifying files or content.
320    Edit,
321    /// Removing files or data.
322    Delete,
323    /// Moving or renaming files.
324    Move,
325    /// Searching for information.
326    Search,
327    /// Running commands or code.
328    Execute,
329    /// Internal reasoning or planning.
330    Think,
331    /// Retrieving external data.
332    Fetch,
333    /// Switching the current session mode.
334    SwitchMode,
335    /// Other tool types (default).
336    #[default]
337    Other,
338    /// Custom or future tool kind.
339    ///
340    /// Values beginning with `_` are reserved for implementation-specific
341    /// extensions. Unknown values that do not begin with `_` are reserved for
342    /// future ACP variants.
343    #[serde(untagged)]
344    Unknown(String),
345}
346
347/// Execution status of a tool call.
348///
349/// Tool calls progress through different statuses during their lifecycle.
350///
351/// See protocol docs: [Status](https://agentclientprotocol.com/protocol/tool-calls#status)
352#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
353#[serde(rename_all = "snake_case")]
354#[non_exhaustive]
355pub enum ToolCallStatus {
356    /// The tool call hasn't started running yet because the input is either
357    /// streaming or we're awaiting approval.
358    #[default]
359    Pending,
360    /// The tool call is currently running.
361    InProgress,
362    /// The tool call completed successfully.
363    Completed,
364    /// The tool call failed with an error.
365    Failed,
366    /// The tool call was cancelled before it completed.
367    Cancelled,
368    /// Custom or future tool call status.
369    ///
370    /// Values beginning with `_` are reserved for implementation-specific
371    /// extensions. Unknown values that do not begin with `_` are reserved for
372    /// future ACP variants.
373    #[serde(untagged)]
374    Other(String),
375}
376
377/// Content produced by a tool call.
378///
379/// Tool calls can produce different types of content including standard
380/// content blocks (text, images), file diffs, or display-only terminals.
381///
382/// See protocol docs: [Content](https://agentclientprotocol.com/protocol/tool-calls#content)
383#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
384#[serde(tag = "type", rename_all = "snake_case")]
385#[non_exhaustive]
386pub enum ToolCallContent {
387    /// Standard content block (text, images, resources).
388    Content(Box<Content>),
389    /// File modification shown as a diff.
390    Diff(Diff),
391    /// A display-only reference to an agent-owned terminal.
392    Terminal(Terminal),
393    /// Custom or future tool call content.
394    ///
395    /// Values beginning with `_` are reserved for implementation-specific
396    /// extensions. Unknown values that do not begin with `_` are reserved for
397    /// future ACP variants.
398    ///
399    /// Receivers that do not understand this content type should preserve the
400    /// raw payload when storing, replaying, proxying, or forwarding tool call
401    /// output, and otherwise ignore it or display it generically.
402    #[serde(untagged)]
403    Other(OtherToolCallContent),
404}
405
406/// Custom or future tool call content payload.
407#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
408#[schemars(inline)]
409#[schemars(transform = other_tool_call_content_schema)]
410#[serde(rename_all = "camelCase")]
411#[non_exhaustive]
412pub struct OtherToolCallContent {
413    /// Custom or future tool call content type.
414    ///
415    /// Values beginning with `_` are reserved for implementation-specific
416    /// extensions. Unknown values that do not begin with `_` are reserved for
417    /// future ACP variants.
418    #[serde(rename = "type")]
419    pub type_: String,
420    /// Additional fields from the unknown tool call content payload.
421    #[serde(flatten)]
422    pub fields: BTreeMap<String, serde_json::Value>,
423}
424
425impl OtherToolCallContent {
426    /// Builds [`OtherToolCallContent`] from an unknown discriminator and preserves the remaining extension fields.
427    #[must_use]
428    pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
429        fields.remove("type");
430        Self {
431            type_: type_.into(),
432            fields,
433        }
434    }
435}
436
437impl<'de> Deserialize<'de> for OtherToolCallContent {
438    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
439    where
440        D: serde::Deserializer<'de>,
441    {
442        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
443        let type_ = fields
444            .remove("type")
445            .ok_or_else(|| serde::de::Error::missing_field("type"))?;
446        let serde_json::Value::String(type_) = type_ else {
447            return Err(serde::de::Error::custom("`type` must be a string"));
448        };
449
450        if is_known_tool_call_content_type(&type_) {
451            return Err(serde::de::Error::custom(format!(
452                "known tool call content `{type_}` did not match its schema"
453            )));
454        }
455
456        Ok(Self { type_, fields })
457    }
458}
459
460fn is_known_tool_call_content_type(type_: &str) -> bool {
461    matches!(type_, "content" | "diff" | "terminal")
462}
463
464fn other_tool_call_content_schema(schema: &mut Schema) {
465    super::schema_util::reject_known_string_discriminators(
466        schema,
467        "type",
468        &["content", "diff", "terminal"],
469    );
470}
471
472impl<T: Into<ContentBlock>> From<T> for ToolCallContent {
473    fn from(content: T) -> Self {
474        ToolCallContent::Content(Box::new(Content::new(content)))
475    }
476}
477
478impl From<Diff> for ToolCallContent {
479    fn from(diff: Diff) -> Self {
480        ToolCallContent::Diff(diff)
481    }
482}
483
484impl From<Terminal> for ToolCallContent {
485    fn from(terminal: Terminal) -> Self {
486        ToolCallContent::Terminal(terminal)
487    }
488}
489
490/// Standard content block (text, images, resources).
491#[serde_as]
492#[skip_serializing_none]
493#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
494#[serde(rename_all = "camelCase")]
495#[non_exhaustive]
496pub struct Content {
497    /// The actual content block.
498    pub content: ContentBlock,
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    #[serde_as(deserialize_as = "DefaultOnError")]
505    #[schemars(extend("x-deserialize-default-on-error" = true))]
506    #[serde(default)]
507    #[serde(rename = "_meta")]
508    pub meta: Option<Meta>,
509}
510
511impl Content {
512    /// Builds [`Content`] with the required fields set; optional fields start unset or empty.
513    #[must_use]
514    pub fn new(content: impl Into<ContentBlock>) -> Self {
515        Self {
516            content: content.into(),
517            meta: None,
518        }
519    }
520
521    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
522    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
523    /// these keys.
524    ///
525    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
526    #[must_use]
527    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
528        self.meta = meta.into_option();
529        self
530    }
531}
532
533/// File changes produced by a tool call.
534///
535/// `changes` is authoritative for affected absolute paths and operations.
536/// `patch` optionally carries renderable text for some or all of those changes
537/// and MUST be consistent with `changes`. Agents SHOULD provide `patch` whenever
538/// feasible. Clients MUST handle diffs where `patch` is omitted or `null`.
539///
540/// See protocol docs: [Content](https://agentclientprotocol.com/protocol/tool-calls#content)
541#[serde_as]
542#[skip_serializing_none]
543#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
544#[serde(rename_all = "camelCase")]
545#[non_exhaustive]
546pub struct Diff {
547    /// Structured file changes described by this diff.
548    ///
549    /// Clients can use this field without parsing patch text to determine affected paths.
550    #[serde_as(deserialize_as = "VecSkipError<_, SkipListener>")]
551    #[schemars(extend("x-deserialize-skip-invalid-items" = true))]
552    pub changes: Vec<DiffChange>,
553    /// Renderable patch text for some or all of the structured changes.
554    ///
555    /// Agents SHOULD provide patch text whenever feasible. Omitted or `null`
556    /// means no renderable patch text was provided.
557    #[serde_as(deserialize_as = "DefaultOnError")]
558    #[schemars(extend("x-deserialize-default-on-error" = true))]
559    #[serde(default)]
560    pub patch: Option<DiffPatch>,
561    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
562    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
563    /// these keys.
564    ///
565    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
566    #[serde_as(deserialize_as = "DefaultOnError")]
567    #[schemars(extend("x-deserialize-default-on-error" = true))]
568    #[serde(default)]
569    #[serde(rename = "_meta")]
570    pub meta: Option<Meta>,
571}
572
573impl Diff {
574    /// Builds [`Diff`] with structured file changes.
575    #[must_use]
576    pub fn new(changes: Vec<DiffChange>) -> Self {
577        Self {
578            changes,
579            patch: None,
580            meta: None,
581        }
582    }
583
584    /// Builds [`Diff`] with Git `--patch` text and structured file changes.
585    #[must_use]
586    pub fn patch(text: impl Into<String>, changes: Vec<DiffChange>) -> Self {
587        Self::new(changes).with_patch(DiffPatch::new(text))
588    }
589
590    /// Sets renderable patch text.
591    #[must_use]
592    pub fn with_patch(mut self, patch: impl IntoOption<DiffPatch>) -> Self {
593        self.patch = patch.into_option();
594        self
595    }
596
597    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
598    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
599    /// these keys.
600    ///
601    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
602    #[must_use]
603    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
604        self.meta = meta.into_option();
605        self
606    }
607}
608
609/// Renderable patch text and its format.
610#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
611#[serde(rename_all = "camelCase")]
612#[non_exhaustive]
613pub struct DiffPatch {
614    /// Patch format. The only ACP-defined value is `git_patch`.
615    pub format: DiffPatchFormat,
616    /// Patch text in the format named by `format`.
617    pub text: String,
618}
619
620impl DiffPatch {
621    /// Builds [`DiffPatch`] with Git `--patch` text.
622    #[must_use]
623    pub fn new(text: impl Into<String>) -> Self {
624        Self {
625            format: DiffPatchFormat::GitPatch,
626            text: text.into(),
627        }
628    }
629}
630
631/// Text patch format used by [`DiffPatch`].
632#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
633#[serde(rename_all = "snake_case")]
634#[non_exhaustive]
635pub enum DiffPatchFormat {
636    /// One or more `diff --git` sections in Git's `--patch` (`-p`) text format.
637    ///
638    /// Paths MUST be absolute. Surrounding commit metadata and email envelopes
639    /// MUST NOT be included.
640    GitPatch,
641    /// Custom or future patch format.
642    ///
643    /// Values beginning with `_` are reserved for implementation-specific
644    /// extensions. Unknown values that do not begin with `_` are reserved for
645    /// future ACP variants.
646    #[serde(untagged)]
647    Other(String),
648}
649
650/// Kind of file content represented by a diff change.
651#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
652#[serde(rename_all = "snake_case")]
653#[non_exhaustive]
654pub enum DiffFileType {
655    /// Text content.
656    Text,
657    /// Binary or otherwise non-text content.
658    Binary,
659    /// Directory entry.
660    Directory,
661    /// Symbolic link.
662    Symlink,
663    /// Custom or future file type.
664    ///
665    /// Values beginning with `_` are reserved for implementation-specific
666    /// extensions. Unknown values that do not begin with `_` are reserved for
667    /// future ACP variants.
668    #[serde(untagged)]
669    Other(String),
670}
671
672/// One file-level change described by a [`Diff`].
673///
674/// Structured change metadata lets clients identify affected files and
675/// operations without parsing the text patch.
676#[serde_as]
677#[skip_serializing_none]
678#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
679#[serde(rename_all = "camelCase")]
680#[non_exhaustive]
681pub struct DiffChange {
682    /// File content kind.
683    ///
684    /// Omitted or `null` means the content kind is unknown.
685    #[serde_as(deserialize_as = "DefaultOnError")]
686    #[schemars(extend("x-deserialize-default-on-error" = true))]
687    #[serde(default)]
688    pub file_type: Option<DiffFileType>,
689    /// MIME type of the file contents.
690    ///
691    /// Omitted or `null` means the MIME type is unknown.
692    #[serde_as(deserialize_as = "DefaultOnError")]
693    #[schemars(extend("x-deserialize-default-on-error" = true))]
694    #[serde(default)]
695    pub mime_type: Option<MediaType>,
696    /// File operation-specific fields.
697    #[serde(flatten)]
698    pub operation: DiffChangeOperation,
699    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
700    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
701    /// these keys.
702    ///
703    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
704    #[serde_as(deserialize_as = "DefaultOnError")]
705    #[schemars(extend("x-deserialize-default-on-error" = true))]
706    #[serde(default)]
707    #[serde(rename = "_meta")]
708    pub meta: Option<Meta>,
709}
710
711impl DiffChange {
712    /// Builds [`DiffChange`] with the required fields set; optional fields start unset or empty.
713    #[must_use]
714    pub fn new(operation: DiffChangeOperation) -> Self {
715        Self {
716            file_type: None,
717            mime_type: None,
718            operation,
719            meta: None,
720        }
721    }
722
723    /// Builds a file add change.
724    #[must_use]
725    pub fn add(path: impl Into<AbsolutePath>) -> Self {
726        Self::new(DiffChangeOperation::Add(DiffPathChange::new(path)))
727    }
728
729    /// Builds a file delete change.
730    #[must_use]
731    pub fn delete(path: impl Into<AbsolutePath>) -> Self {
732        Self::new(DiffChangeOperation::Delete(DiffPathChange::new(path)))
733    }
734
735    /// Builds a file modify change.
736    #[must_use]
737    pub fn modify(path: impl Into<AbsolutePath>) -> Self {
738        Self::new(DiffChangeOperation::Modify(DiffPathChange::new(path)))
739    }
740
741    /// Builds a file move or rename change.
742    #[must_use]
743    pub fn move_file(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
744        Self::new(DiffChangeOperation::Move(DiffPathPairChange::new(
745            old_path, path,
746        )))
747    }
748
749    /// Builds a file copy change.
750    #[must_use]
751    pub fn copy(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
752        Self::new(DiffChangeOperation::Copy(DiffPathPairChange::new(
753            old_path, path,
754        )))
755    }
756
757    /// File content kind.
758    ///
759    /// Omitted or `null` means the content kind is unknown.
760    #[must_use]
761    pub fn file_type(mut self, file_type: impl IntoOption<DiffFileType>) -> Self {
762        self.file_type = file_type.into_option();
763        self
764    }
765
766    /// MIME type of the file contents.
767    ///
768    /// Omitted or `null` means the MIME type is unknown.
769    #[must_use]
770    pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
771        self.mime_type = mime_type.into_option();
772        self
773    }
774
775    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
776    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
777    /// these keys.
778    ///
779    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
780    #[must_use]
781    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
782        self.meta = meta.into_option();
783        self
784    }
785}
786
787/// File operation for a [`DiffChange`].
788#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
789#[serde(tag = "operation", rename_all = "snake_case")]
790#[non_exhaustive]
791pub enum DiffChangeOperation {
792    /// A file was added.
793    Add(DiffPathChange),
794    /// A file was deleted.
795    Delete(DiffPathChange),
796    /// A file was modified in place.
797    Modify(DiffPathChange),
798    /// A file was moved or renamed.
799    Move(DiffPathPairChange),
800    /// A file was copied.
801    Copy(DiffPathPairChange),
802    /// Custom or future file operation.
803    ///
804    /// Values beginning with `_` are reserved for implementation-specific
805    /// extensions. Unknown values that do not begin with `_` are reserved for
806    /// future ACP variants.
807    #[serde(untagged)]
808    Other(OtherDiffChange),
809}
810
811/// Operation metadata for add, delete, and modify changes.
812#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
813#[serde(rename_all = "camelCase")]
814#[non_exhaustive]
815pub struct DiffPathChange {
816    /// Absolute path for the operation.
817    pub path: AbsolutePath,
818}
819
820impl DiffPathChange {
821    /// Builds [`DiffPathChange`] with the required fields set; optional fields start unset or empty.
822    #[must_use]
823    pub fn new(path: impl Into<AbsolutePath>) -> Self {
824        Self { path: path.into() }
825    }
826}
827
828/// Operation metadata for move and copy changes.
829#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
830#[serde(rename_all = "camelCase")]
831#[non_exhaustive]
832pub struct DiffPathPairChange {
833    /// Absolute path before the operation.
834    pub old_path: AbsolutePath,
835    /// Absolute path after the operation.
836    pub path: AbsolutePath,
837}
838
839impl DiffPathPairChange {
840    /// Builds [`DiffPathPairChange`] with the required fields set; optional fields start unset or empty.
841    #[must_use]
842    pub fn new(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
843        Self {
844            old_path: old_path.into(),
845            path: path.into(),
846        }
847    }
848}
849
850/// Custom or future file operation payload.
851#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
852#[schemars(inline)]
853#[schemars(transform = other_diff_change_schema)]
854#[serde(rename_all = "camelCase")]
855#[non_exhaustive]
856pub struct OtherDiffChange {
857    /// Custom or future file operation.
858    ///
859    /// Values beginning with `_` are reserved for implementation-specific
860    /// extensions. Unknown values that do not begin with `_` are reserved for
861    /// future ACP variants.
862    pub operation: String,
863    /// Additional fields from the unknown file operation payload.
864    #[serde(flatten)]
865    pub fields: BTreeMap<String, serde_json::Value>,
866}
867
868impl OtherDiffChange {
869    /// Builds [`OtherDiffChange`] from an unknown discriminator and preserves the remaining extension fields.
870    #[must_use]
871    pub fn new(
872        operation: impl Into<String>,
873        mut fields: BTreeMap<String, serde_json::Value>,
874    ) -> Self {
875        fields.remove("operation");
876        fields.remove("fileType");
877        fields.remove("mimeType");
878        fields.remove("_meta");
879        Self {
880            operation: operation.into(),
881            fields,
882        }
883    }
884}
885
886impl<'de> Deserialize<'de> for OtherDiffChange {
887    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
888    where
889        D: serde::Deserializer<'de>,
890    {
891        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
892        let operation = fields
893            .remove("operation")
894            .ok_or_else(|| serde::de::Error::missing_field("operation"))?;
895        let serde_json::Value::String(operation) = operation else {
896            return Err(serde::de::Error::custom("`operation` must be a string"));
897        };
898
899        if is_known_diff_change_operation(&operation) {
900            return Err(serde::de::Error::custom(format!(
901                "known diff change operation `{operation}` did not match its schema"
902            )));
903        }
904        fields.remove("fileType");
905        fields.remove("mimeType");
906        fields.remove("_meta");
907
908        Ok(Self { operation, fields })
909    }
910}
911
912fn is_known_diff_change_operation(operation: &str) -> bool {
913    matches!(operation, "add" | "delete" | "modify" | "move" | "copy")
914}
915
916fn other_diff_change_schema(schema: &mut Schema) {
917    super::schema_util::reject_known_string_discriminators(
918        schema,
919        "operation",
920        &["add", "delete", "modify", "move", "copy"],
921    );
922}
923
924/// A file location being accessed or modified by a tool.
925///
926/// Enables clients to implement "follow-along" features that track
927/// which files the agent is working with in real-time.
928///
929/// See protocol docs: [Following the Agent](https://agentclientprotocol.com/protocol/tool-calls#following-the-agent)
930#[serde_as]
931#[skip_serializing_none]
932#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
933#[serde(rename_all = "camelCase")]
934#[non_exhaustive]
935pub struct ToolCallLocation {
936    /// The absolute file path being accessed or modified.
937    pub path: AbsolutePath,
938    /// Optional line number within the file.
939    #[serde_as(deserialize_as = "DefaultOnError")]
940    #[schemars(extend("x-deserialize-default-on-error" = true))]
941    #[serde(default)]
942    pub line: Option<u32>,
943    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
944    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
945    /// these keys.
946    ///
947    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
948    #[serde_as(deserialize_as = "DefaultOnError")]
949    #[schemars(extend("x-deserialize-default-on-error" = true))]
950    #[serde(default)]
951    #[serde(rename = "_meta")]
952    pub meta: Option<Meta>,
953}
954
955impl ToolCallLocation {
956    /// Builds [`ToolCallLocation`] with the required fields set; optional fields start unset or empty.
957    #[must_use]
958    pub fn new(path: impl Into<AbsolutePath>) -> Self {
959        Self {
960            path: path.into(),
961            line: None,
962            meta: None,
963        }
964    }
965
966    /// Optional line number within the file.
967    #[must_use]
968    pub fn line(mut self, line: impl IntoOption<u32>) -> Self {
969        self.line = line.into_option();
970        self
971    }
972
973    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
974    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
975    /// these keys.
976    ///
977    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
978    #[must_use]
979    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
980        self.meta = meta.into_option();
981        self
982    }
983}
984
985#[cfg(test)]
986mod tests {
987    use super::*;
988    use crate::MaybeUndefined;
989
990    #[test]
991    fn tool_call_serializes_as_upsert() {
992        let tool_call = ToolCallUpdate::new("tc_1")
993            .title("Reading configuration")
994            .status(ToolCallStatus::InProgress)
995            .raw_input(serde_json::json!({"path": "settings.json"}));
996
997        assert_eq!(
998            serde_json::to_value(tool_call).unwrap(),
999            serde_json::json!({
1000                "toolCallId": "tc_1",
1001                "title": "Reading configuration",
1002                "status": "in_progress",
1003                "rawInput": {
1004                    "path": "settings.json"
1005                }
1006            })
1007        );
1008    }
1009
1010    #[test]
1011    fn tool_call_update_distinguishes_omitted_null_and_value() {
1012        let tool_call = ToolCallUpdate::new("tc_1")
1013            .status(ToolCallStatus::Completed)
1014            .content(None::<Vec<ToolCallContent>>);
1015
1016        assert_eq!(
1017            serde_json::to_value(tool_call).unwrap(),
1018            serde_json::json!({
1019                "toolCallId": "tc_1",
1020                "status": "completed",
1021                "content": null
1022            })
1023        );
1024
1025        let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1026            "toolCallId": "tc_1",
1027            "status": null,
1028            "locations": []
1029        }))
1030        .unwrap();
1031        assert_eq!(deserialized.title, MaybeUndefined::Undefined);
1032        assert_eq!(deserialized.status, MaybeUndefined::Null);
1033        assert_eq!(deserialized.locations, MaybeUndefined::Value(Vec::new()));
1034    }
1035
1036    #[cfg(feature = "unstable_tool_call_name")]
1037    #[test]
1038    fn tool_call_name_patch_distinguishes_omitted_null_and_value() {
1039        let named = ToolCallUpdate::new("tc_1").name("read_file");
1040        assert_eq!(
1041            serde_json::to_value(named).unwrap(),
1042            serde_json::json!({
1043                "toolCallId": "tc_1",
1044                "name": "read_file"
1045            })
1046        );
1047
1048        let omitted = ToolCallUpdate::new("tc_1");
1049        assert_eq!(omitted.name, MaybeUndefined::Undefined);
1050
1051        let from_null: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1052            "toolCallId": "tc_1",
1053            "name": null
1054        }))
1055        .unwrap();
1056        assert_eq!(from_null.name, MaybeUndefined::Null);
1057
1058        let mut stored = ToolCallUpdate::new("tc_1").name("read_file");
1059        stored.apply_update(ToolCallUpdate::new("tc_1"));
1060        assert_eq!(stored.name, MaybeUndefined::Value("read_file".to_string()));
1061
1062        stored.apply_update(ToolCallUpdate::new("tc_1").name(None::<String>));
1063        assert_eq!(stored.name, MaybeUndefined::Null);
1064
1065        stored.apply_update(ToolCallUpdate::new("tc_1").name("write_file"));
1066        assert_eq!(stored.name, MaybeUndefined::Value("write_file".to_string()));
1067    }
1068
1069    #[test]
1070    fn tool_call_update_distinguishes_meta_omitted_null_and_value() {
1071        let mut meta = Meta::new();
1072        meta.insert("source".to_string(), serde_json::json!("tool-call"));
1073
1074        assert_eq!(
1075            serde_json::to_value(ToolCallUpdate::new("tc_1").meta(meta.clone())).unwrap(),
1076            serde_json::json!({
1077                "toolCallId": "tc_1",
1078                "_meta": {
1079                    "source": "tool-call"
1080                }
1081            })
1082        );
1083
1084        assert_eq!(
1085            serde_json::to_value(ToolCallUpdate::new("tc_1").meta(None::<Meta>)).unwrap(),
1086            serde_json::json!({
1087                "toolCallId": "tc_1",
1088                "_meta": null
1089            })
1090        );
1091
1092        let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1093            "toolCallId": "tc_1",
1094            "_meta": null
1095        }))
1096        .unwrap();
1097        assert_eq!(deserialized.meta, MaybeUndefined::Null);
1098
1099        let patch = ToolCallUpdate::new("tc_1");
1100        assert_eq!(patch.meta, MaybeUndefined::Undefined);
1101
1102        let mut stored = ToolCallUpdate::new("tc_1").meta(meta);
1103        stored.apply_update(ToolCallUpdate::new("tc_1").meta(None::<Meta>));
1104        assert_eq!(stored.meta, MaybeUndefined::Null);
1105    }
1106
1107    #[test]
1108    fn tool_call_update_skips_malformed_list_items() {
1109        let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1110            "toolCallId": "tc_1",
1111            "content": [
1112                {
1113                    "type": "content",
1114                    "content": {
1115                        "type": "text",
1116                        "text": "ok"
1117                    }
1118                },
1119                {
1120                    "type": "diff",
1121                    "path": "/bad"
1122                }
1123            ],
1124            "locations": [
1125                {
1126                    "path": "/ok",
1127                    "line": 3
1128                },
1129                {
1130                    "line": 4
1131                }
1132            ]
1133        }))
1134        .unwrap();
1135
1136        let MaybeUndefined::Value(content) = deserialized.content else {
1137            panic!("content should deserialize to a value");
1138        };
1139        assert_eq!(content.len(), 1);
1140
1141        let MaybeUndefined::Value(locations) = deserialized.locations else {
1142            panic!("locations should deserialize to a value");
1143        };
1144        assert_eq!(locations.len(), 1);
1145    }
1146
1147    #[test]
1148    fn tool_call_content_chunk_serializes_single_content_item() {
1149        let chunk = ToolCallContentChunk::new(
1150            "tc_1",
1151            ContentBlock::Text(crate::v2::TextContent::new("partial output")),
1152        );
1153
1154        assert_eq!(
1155            serde_json::to_value(chunk).unwrap(),
1156            serde_json::json!({
1157                "toolCallId": "tc_1",
1158                "content": {
1159                    "type": "content",
1160                    "content": {
1161                        "type": "text",
1162                        "text": "partial output"
1163                    }
1164                }
1165            })
1166        );
1167    }
1168
1169    #[test]
1170    fn terminal_content_serializes_as_display_reference() {
1171        let terminal = ToolCallContent::from(Terminal::new("term_1"));
1172
1173        assert_eq!(
1174            serde_json::to_value(terminal).unwrap(),
1175            serde_json::json!({
1176                "type": "terminal",
1177                "terminalId": "term_1"
1178            })
1179        );
1180    }
1181
1182    #[test]
1183    fn diff_patch_serializes_git_patch_with_structured_changes() {
1184        let patch_text = "diff --git /repo/config.json /repo/config.json\n--- /repo/config.json\n+++ /repo/config.json\n@@ -1 +1 @@\n-old\n+new\n";
1185        let diff = ToolCallContent::Diff(Diff::patch(
1186            patch_text,
1187            vec![
1188                DiffChange::modify("/repo/config.json")
1189                    .file_type(DiffFileType::Text)
1190                    .mime_type("application/json"),
1191            ],
1192        ));
1193
1194        assert_eq!(
1195            serde_json::to_value(diff).unwrap(),
1196            serde_json::json!({
1197                "type": "diff",
1198                "changes": [
1199                    {
1200                        "operation": "modify",
1201                        "path": "/repo/config.json",
1202                        "fileType": "text",
1203                        "mimeType": "application/json"
1204                    }
1205                ],
1206                "patch": {
1207                    "format": "git_patch",
1208                    "text": patch_text
1209                }
1210            })
1211        );
1212    }
1213
1214    #[test]
1215    fn diff_patch_requires_text() {
1216        let result = serde_json::from_value::<DiffPatch>(serde_json::json!({
1217            "format": "git_patch",
1218            "diff": "diff --git /repo/config.json /repo/config.json\n"
1219        }));
1220
1221        assert!(result.is_err());
1222    }
1223
1224    #[test]
1225    fn diff_serializes_binary_modify_without_patch_text() {
1226        let diff = ToolCallContent::Diff(Diff::new(vec![
1227            DiffChange::modify("/repo/assets/logo.png")
1228                .file_type(DiffFileType::Binary)
1229                .mime_type("image/png"),
1230        ]));
1231
1232        assert_eq!(
1233            serde_json::to_value(diff).unwrap(),
1234            serde_json::json!({
1235                "type": "diff",
1236                "changes": [
1237                    {
1238                        "operation": "modify",
1239                        "path": "/repo/assets/logo.png",
1240                        "fileType": "binary",
1241                        "mimeType": "image/png"
1242                    }
1243                ]
1244            })
1245        );
1246    }
1247
1248    #[test]
1249    fn diff_move_serializes_shared_fields_with_operation_payload() {
1250        let diff = ToolCallContent::Diff(Diff::new(vec![
1251            DiffChange::move_file("/repo/src/old.rs", "/repo/src/new.rs")
1252                .file_type(DiffFileType::Text)
1253                .mime_type("text/rust"),
1254        ]));
1255
1256        assert_eq!(
1257            serde_json::to_value(diff).unwrap(),
1258            serde_json::json!({
1259                "type": "diff",
1260                "changes": [
1261                    {
1262                        "operation": "move",
1263                        "oldPath": "/repo/src/old.rs",
1264                        "path": "/repo/src/new.rs",
1265                        "fileType": "text",
1266                        "mimeType": "text/rust"
1267                    }
1268                ]
1269            })
1270        );
1271    }
1272
1273    #[test]
1274    fn diff_changes_skip_malformed_list_items() {
1275        let patch_text = "diff --git /ok /ok\ndeleted file mode 100644\n--- /ok\n+++ /dev/null\n@@ -1 +0,0 @@\n-old\n";
1276        let content: ToolCallContent = serde_json::from_value(serde_json::json!({
1277            "type": "diff",
1278            "changes": [
1279                {
1280                    "operation": "modify"
1281                },
1282                {
1283                    "operation": "delete",
1284                    "path": "/ok"
1285                }
1286            ],
1287            "patch": {
1288                "format": "git_patch",
1289                "text": patch_text
1290            }
1291        }))
1292        .unwrap();
1293
1294        let ToolCallContent::Diff(diff) = content else {
1295            panic!("expected diff content");
1296        };
1297        assert_eq!(diff.changes, vec![DiffChange::delete("/ok")]);
1298        assert_eq!(diff.patch, Some(DiffPatch::new(patch_text)));
1299    }
1300
1301    #[test]
1302    fn tool_kind_preserves_unknown_variant() {
1303        let kind: ToolKind = serde_json::from_str("\"review\"").unwrap();
1304        assert_eq!(kind, ToolKind::Unknown("review".to_string()));
1305        assert_eq!(serde_json::to_value(&kind).unwrap(), "review");
1306    }
1307
1308    #[test]
1309    fn tool_call_status_preserves_unknown_variant() {
1310        let status: ToolCallStatus = serde_json::from_str("\"deferred\"").unwrap();
1311        assert_eq!(status, ToolCallStatus::Other("deferred".to_string()));
1312        assert_eq!(serde_json::to_value(&status).unwrap(), "deferred");
1313    }
1314
1315    #[test]
1316    fn tool_call_status_recognizes_cancelled_variant() {
1317        let status: ToolCallStatus = serde_json::from_str("\"cancelled\"").unwrap();
1318        assert_eq!(status, ToolCallStatus::Cancelled);
1319        assert_eq!(serde_json::to_value(&status).unwrap(), "cancelled");
1320    }
1321
1322    #[test]
1323    fn tool_call_content_preserves_unknown_variant() {
1324        let content: ToolCallContent = serde_json::from_value(serde_json::json!({
1325            "type": "_chart",
1326            "title": "Tests",
1327            "data": [1, 2, 3]
1328        }))
1329        .unwrap();
1330
1331        let ToolCallContent::Other(unknown) = content else {
1332            panic!("expected unknown tool call content");
1333        };
1334
1335        assert_eq!(unknown.type_, "_chart");
1336        assert_eq!(
1337            unknown.fields.get("title"),
1338            Some(&serde_json::json!("Tests"))
1339        );
1340        assert_eq!(
1341            serde_json::to_value(ToolCallContent::Other(unknown)).unwrap(),
1342            serde_json::json!({
1343                "type": "_chart",
1344                "title": "Tests",
1345                "data": [1, 2, 3]
1346            })
1347        );
1348    }
1349
1350    #[test]
1351    fn tool_call_content_does_not_hide_malformed_known_variant() {
1352        assert!(
1353            serde_json::from_value::<ToolCallContent>(serde_json::json!({
1354                "type": "diff"
1355            }))
1356            .is_err()
1357        );
1358        assert!(
1359            serde_json::from_value::<ToolCallContent>(serde_json::json!({
1360                "type": "terminal"
1361            }))
1362            .is_err()
1363        );
1364    }
1365}