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