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