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