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 use client 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 make assumptions about values at these keys.
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 make assumptions about values at these keys.
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 make assumptions about values at
279    /// these keys. 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 make assumptions about values at these keys.
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 make assumptions about values at
535    /// these keys.
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 make assumptions about values at these keys.
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 make assumptions about values at
612    /// these keys.
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 make assumptions about values at these keys.
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 make assumptions about values at
659    /// these keys.
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. Surrounding commit metadata and email envelopes MUST NOT be
684    /// included.
685    GitPatch,
686    /// Custom or future patch format.
687    ///
688    /// Values beginning with `_` are reserved for implementation-specific
689    /// extensions. Unknown values that do not begin with `_` are reserved for
690    /// future ACP variants.
691    #[serde(untagged)]
692    Other(String),
693}
694
695/// Kind of file content represented by a diff change.
696#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
697#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
698#[serde(rename_all = "snake_case")]
699#[non_exhaustive]
700pub enum DiffFileType {
701    /// Text content.
702    Text,
703    /// Binary or otherwise non-text content.
704    Binary,
705    /// Directory entry.
706    Directory,
707    /// Symbolic link.
708    Symlink,
709    /// Custom or future file type.
710    ///
711    /// Values beginning with `_` are reserved for implementation-specific
712    /// extensions. Unknown values that do not begin with `_` are reserved for
713    /// future ACP variants.
714    #[serde(untagged)]
715    Other(String),
716}
717
718/// One file-level change described by a `Diff`.
719///
720/// Structured change metadata lets clients identify affected files and
721/// operations without parsing the text patch.
722#[serde_as]
723#[skip_serializing_none]
724#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
725#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
726#[serde(rename_all = "camelCase")]
727#[non_exhaustive]
728pub struct DiffChange {
729    /// File content kind after the operation (for `delete`, of the deleted file).
730    ///
731    /// Omitted and `null` are equivalent and mean the content kind is unknown.
732    #[serde_as(deserialize_as = "DefaultOnError")]
733    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
734    #[serde(default)]
735    pub file_type: Option<DiffFileType>,
736    /// MIME type of the file contents after the operation (for `delete`, of the deleted file).
737    ///
738    /// Omitted and `null` are equivalent and mean the MIME type is unknown.
739    #[serde_as(deserialize_as = "DefaultOnError")]
740    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
741    #[serde(default)]
742    pub mime_type: Option<MediaType>,
743    /// File operation-specific fields.
744    #[serde(flatten)]
745    pub operation: DiffChangeOperation,
746    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
747    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
748    /// Implementations MUST NOT make assumptions about values at these keys.
749    ///
750    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
751    #[serde_as(deserialize_as = "DefaultOnError")]
752    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
753    #[serde(default)]
754    #[serde(rename = "_meta")]
755    pub meta: Option<Meta>,
756}
757
758impl DiffChange {
759    /// Builds [`DiffChange`] with the required fields set; optional fields start unset or empty.
760    #[must_use]
761    pub fn new(operation: DiffChangeOperation) -> Self {
762        Self {
763            file_type: None,
764            mime_type: None,
765            operation,
766            meta: None,
767        }
768    }
769
770    /// Builds a file add change.
771    #[must_use]
772    pub fn add(path: impl Into<AbsolutePath>) -> Self {
773        Self::new(DiffChangeOperation::Add(DiffPathChange::new(path)))
774    }
775
776    /// Builds a file delete change.
777    #[must_use]
778    pub fn delete(path: impl Into<AbsolutePath>) -> Self {
779        Self::new(DiffChangeOperation::Delete(DiffPathChange::new(path)))
780    }
781
782    /// Builds a file modify change.
783    #[must_use]
784    pub fn modify(path: impl Into<AbsolutePath>) -> Self {
785        Self::new(DiffChangeOperation::Modify(DiffPathChange::new(path)))
786    }
787
788    /// Builds a file move or rename change.
789    #[must_use]
790    pub fn move_file(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
791        Self::new(DiffChangeOperation::Move(DiffPathPairChange::new(
792            old_path, path,
793        )))
794    }
795
796    /// Builds a file copy change.
797    #[must_use]
798    pub fn copy(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
799        Self::new(DiffChangeOperation::Copy(DiffPathPairChange::new(
800            old_path, path,
801        )))
802    }
803
804    /// File content kind.
805    ///
806    /// Omitted and `null` are equivalent and mean the content kind is unknown.
807    #[must_use]
808    pub fn file_type(mut self, file_type: impl IntoOption<DiffFileType>) -> Self {
809        self.file_type = file_type.into_option();
810        self
811    }
812
813    /// MIME type of the file contents.
814    ///
815    /// Omitted and `null` are equivalent and mean the MIME type is unknown.
816    #[must_use]
817    pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
818        self.mime_type = mime_type.into_option();
819        self
820    }
821
822    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
823    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
824    /// these keys.
825    ///
826    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
827    #[must_use]
828    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
829        self.meta = meta.into_option();
830        self
831    }
832}
833
834/// File operation for a [`DiffChange`].
835#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
836#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
837#[serde(tag = "operation", rename_all = "snake_case")]
838#[non_exhaustive]
839pub enum DiffChangeOperation {
840    /// A file was added.
841    Add(DiffPathChange),
842    /// A file was deleted.
843    Delete(DiffPathChange),
844    /// A file was modified in place.
845    Modify(DiffPathChange),
846    /// A file was moved or renamed.
847    Move(DiffPathPairChange),
848    /// A file was copied.
849    Copy(DiffPathPairChange),
850    /// Custom or future file operation.
851    ///
852    /// Values beginning with `_` are reserved for implementation-specific
853    /// extensions. Unknown values that do not begin with `_` are reserved for
854    /// future ACP variants.
855    #[serde(untagged)]
856    Other(OtherDiffChange),
857}
858
859/// Operation metadata for add, delete, and modify changes.
860#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
861#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
862#[serde(rename_all = "camelCase")]
863#[non_exhaustive]
864pub struct DiffPathChange {
865    /// Absolute path for the operation.
866    pub path: AbsolutePath,
867}
868
869impl DiffPathChange {
870    /// Builds [`DiffPathChange`] with the required fields set; optional fields start unset or empty.
871    #[must_use]
872    pub fn new(path: impl Into<AbsolutePath>) -> Self {
873        Self { path: path.into() }
874    }
875}
876
877/// Operation metadata for move and copy changes.
878#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
879#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
880#[serde(rename_all = "camelCase")]
881#[non_exhaustive]
882pub struct DiffPathPairChange {
883    /// Absolute path before the operation.
884    pub old_path: AbsolutePath,
885    /// Absolute path after the operation.
886    pub path: AbsolutePath,
887}
888
889impl DiffPathPairChange {
890    /// Builds [`DiffPathPairChange`] with the required fields set; optional fields start unset or empty.
891    #[must_use]
892    pub fn new(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
893        Self {
894            old_path: old_path.into(),
895            path: path.into(),
896        }
897    }
898}
899
900/// Custom or future file operation payload.
901#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
902#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
903#[cfg_attr(feature = "schemars", schemars(inline))]
904#[cfg_attr(feature = "schemars", schemars(transform = other_diff_change_schema))]
905#[serde(rename_all = "camelCase")]
906#[non_exhaustive]
907pub struct OtherDiffChange {
908    /// Custom or future file operation.
909    ///
910    /// Values beginning with `_` are reserved for implementation-specific
911    /// extensions. Unknown values that do not begin with `_` are reserved for
912    /// future ACP variants.
913    pub operation: String,
914    /// Additional fields from the unknown file operation payload.
915    #[serde(flatten)]
916    pub fields: BTreeMap<String, serde_json::Value>,
917}
918
919impl OtherDiffChange {
920    /// Builds [`OtherDiffChange`] from an unknown discriminator and preserves the remaining extension fields.
921    #[must_use]
922    pub fn new(
923        operation: impl Into<String>,
924        mut fields: BTreeMap<String, serde_json::Value>,
925    ) -> Self {
926        fields.remove("operation");
927        fields.remove("fileType");
928        fields.remove("mimeType");
929        fields.remove("_meta");
930        Self {
931            operation: operation.into(),
932            fields,
933        }
934    }
935}
936
937impl<'de> Deserialize<'de> for OtherDiffChange {
938    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
939    where
940        D: serde::Deserializer<'de>,
941    {
942        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
943        let operation = fields
944            .remove("operation")
945            .ok_or_else(|| serde::de::Error::missing_field("operation"))?;
946        let serde_json::Value::String(operation) = operation else {
947            return Err(serde::de::Error::custom("`operation` must be a string"));
948        };
949
950        if is_known_diff_change_operation(&operation) {
951            return Err(serde::de::Error::custom(format!(
952                "known diff change operation `{operation}` did not match its schema"
953            )));
954        }
955        fields.remove("fileType");
956        fields.remove("mimeType");
957        fields.remove("_meta");
958
959        Ok(Self { operation, fields })
960    }
961}
962
963fn is_known_diff_change_operation(operation: &str) -> bool {
964    matches!(operation, "add" | "delete" | "modify" | "move" | "copy")
965}
966
967#[cfg(feature = "schemars")]
968fn other_diff_change_schema(schema: &mut Schema) {
969    super::schema_util::reject_known_string_discriminators(
970        schema,
971        "operation",
972        &["add", "delete", "modify", "move", "copy"],
973    );
974}
975
976/// A file location being accessed or modified by a tool.
977///
978/// Enables clients to implement "follow-along" features that track
979/// which files the agent is working with in real-time.
980///
981/// See protocol docs: [Following the Agent](https://agentclientprotocol.com/protocol/v2/tool-calls#following-the-agent)
982#[serde_as]
983#[skip_serializing_none]
984#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
985#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
986#[serde(rename_all = "camelCase")]
987#[non_exhaustive]
988pub struct ToolCallLocation {
989    /// The absolute file path being accessed or modified.
990    pub path: AbsolutePath,
991    /// Optional 1-based line number within the file. Omitted and `null` are equivalent and
992    /// mean no specific line; `0` is invalid.
993    #[serde_as(deserialize_as = "DefaultOnError")]
994    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
995    #[serde(default)]
996    pub line: Option<std::num::NonZeroU32>,
997    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
998    /// metadata to their interactions. Omitted and `null` are equivalent and mean no metadata.
999    /// Implementations MUST NOT make assumptions about values at these keys.
1000    ///
1001    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
1002    #[serde_as(deserialize_as = "DefaultOnError")]
1003    #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1004    #[serde(default)]
1005    #[serde(rename = "_meta")]
1006    pub meta: Option<Meta>,
1007}
1008
1009impl ToolCallLocation {
1010    /// Builds [`ToolCallLocation`] with the required fields set; optional fields start unset or empty.
1011    #[must_use]
1012    pub fn new(path: impl Into<AbsolutePath>) -> Self {
1013        Self {
1014            path: path.into(),
1015            line: None,
1016            meta: None,
1017        }
1018    }
1019
1020    /// Optional line number within the file.
1021    ///
1022    /// Line numbers are 1-based; `0` means no specific line.
1023    #[must_use]
1024    pub fn line(mut self, line: impl IntoOption<u32>) -> Self {
1025        self.line = line.into_option().and_then(std::num::NonZeroU32::new);
1026        self
1027    }
1028
1029    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
1030    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
1031    /// these keys.
1032    ///
1033    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)
1034    #[must_use]
1035    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
1036        self.meta = meta.into_option();
1037        self
1038    }
1039}
1040
1041#[cfg(test)]
1042mod tests {
1043
1044    #[test]
1045    fn location_line_is_one_based() {
1046        let location: ToolCallLocation =
1047            serde_json::from_value(serde_json::json!({ "path": "/a.rs", "line": 0 })).unwrap();
1048        assert_eq!(location.line, None);
1049        let location: ToolCallLocation =
1050            serde_json::from_value(serde_json::json!({ "path": "/a.rs", "line": 3 })).unwrap();
1051        assert_eq!(location.line.map(std::num::NonZeroU32::get), Some(3));
1052        assert_eq!(ToolCallLocation::new("/a.rs").line(0).line, None);
1053    }
1054
1055    use super::*;
1056    use crate::MaybeUndefined;
1057    use serde_json::{from_value, json, to_value};
1058
1059    #[test]
1060    fn tool_call_serializes_as_upsert() {
1061        let tool_call = ToolCallUpdate::new("tc_1")
1062            .title("Reading configuration")
1063            .status(ToolCallStatus::InProgress)
1064            .raw_input(serde_json::json!({"path": "settings.json"}));
1065
1066        assert_eq!(
1067            serde_json::to_value(tool_call).unwrap(),
1068            serde_json::json!({
1069                "toolCallId": "tc_1",
1070                "title": "Reading configuration",
1071                "status": "in_progress",
1072                "rawInput": {
1073                    "path": "settings.json"
1074                }
1075            })
1076        );
1077    }
1078
1079    #[test]
1080    fn tool_call_update_distinguishes_omitted_null_and_value() {
1081        let tool_call = ToolCallUpdate::new("tc_1")
1082            .status(ToolCallStatus::Completed)
1083            .content(None::<Vec<ToolCallContent>>);
1084
1085        assert_eq!(
1086            serde_json::to_value(tool_call).unwrap(),
1087            serde_json::json!({
1088                "toolCallId": "tc_1",
1089                "status": "completed",
1090                "content": null
1091            })
1092        );
1093
1094        let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1095            "toolCallId": "tc_1",
1096            "status": null,
1097            "locations": []
1098        }))
1099        .unwrap();
1100        assert_eq!(deserialized.title, MaybeUndefined::Undefined);
1101        assert_eq!(deserialized.status, MaybeUndefined::Null);
1102        assert_eq!(deserialized.locations, MaybeUndefined::Value(Vec::new()));
1103    }
1104
1105    #[test]
1106    fn tool_call_name_patch_distinguishes_omitted_null_and_value() {
1107        let named = ToolCallUpdate::new("tc_1").name("read_file");
1108        assert_eq!(
1109            serde_json::to_value(named).unwrap(),
1110            serde_json::json!({
1111                "toolCallId": "tc_1",
1112                "name": "read_file"
1113            })
1114        );
1115
1116        let omitted = ToolCallUpdate::new("tc_1");
1117        assert_eq!(omitted.name, MaybeUndefined::Undefined);
1118
1119        let from_null: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1120            "toolCallId": "tc_1",
1121            "name": null
1122        }))
1123        .unwrap();
1124        assert_eq!(from_null.name, MaybeUndefined::Null);
1125
1126        let mut stored = ToolCallUpdate::new("tc_1").name("read_file");
1127        stored.apply_update(ToolCallUpdate::new("tc_1"));
1128        assert_eq!(stored.name, MaybeUndefined::Value("read_file".to_string()));
1129
1130        stored.apply_update(ToolCallUpdate::new("tc_1").name(None::<String>));
1131        assert_eq!(stored.name, MaybeUndefined::Null);
1132
1133        stored.apply_update(ToolCallUpdate::new("tc_1").name("write_file"));
1134        assert_eq!(stored.name, MaybeUndefined::Value("write_file".to_string()));
1135    }
1136
1137    #[test]
1138    fn tool_call_update_distinguishes_meta_omitted_null_and_value() {
1139        let mut meta = Meta::new();
1140        meta.insert("source".to_string(), serde_json::json!("tool-call"));
1141
1142        assert_eq!(
1143            serde_json::to_value(ToolCallUpdate::new("tc_1").meta(meta.clone())).unwrap(),
1144            serde_json::json!({
1145                "toolCallId": "tc_1",
1146                "_meta": {
1147                    "source": "tool-call"
1148                }
1149            })
1150        );
1151
1152        assert_eq!(
1153            serde_json::to_value(ToolCallUpdate::new("tc_1").meta(None::<Meta>)).unwrap(),
1154            serde_json::json!({
1155                "toolCallId": "tc_1",
1156                "_meta": null
1157            })
1158        );
1159
1160        let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1161            "toolCallId": "tc_1",
1162            "_meta": null
1163        }))
1164        .unwrap();
1165        assert_eq!(deserialized.meta, MaybeUndefined::Null);
1166
1167        let patch = ToolCallUpdate::new("tc_1");
1168        assert_eq!(patch.meta, MaybeUndefined::Undefined);
1169
1170        let mut stored = ToolCallUpdate::new("tc_1").meta(meta);
1171        stored.apply_update(ToolCallUpdate::new("tc_1").meta(None::<Meta>));
1172        assert_eq!(stored.meta, MaybeUndefined::Null);
1173    }
1174
1175    #[test]
1176    fn tool_call_update_patches_meta_by_top_level_key() {
1177        let mut stored: ToolCallUpdate = from_value(json!({
1178            "toolCallId": "tc_1",
1179            "_meta": {"zed.dev/a": 1, "acme.dev/b": {"x": 1}}
1180        }))
1181        .unwrap();
1182
1183        // A writer that only knows its own key must not erase other keys.
1184        stored.apply_update(
1185            from_value(json!({"toolCallId": "tc_1", "_meta": {"proxy.dev/c": true}})).unwrap(),
1186        );
1187        stored.apply_update(
1188            from_value(json!({
1189                "toolCallId": "tc_1",
1190                "_meta": {"zed.dev/a": null, "acme.dev/b": {"y": 2}}
1191            }))
1192            .unwrap(),
1193        );
1194
1195        assert_eq!(
1196            to_value(&stored).unwrap(),
1197            json!({
1198                "toolCallId": "tc_1",
1199                "_meta": {"acme.dev/b": {"y": 2}, "proxy.dev/c": true}
1200            })
1201        );
1202    }
1203
1204    #[test]
1205    fn tool_call_wire_patches_preserve_omitted_fields_and_replace_values() {
1206        let initial = json!({
1207            "toolCallId": "tc_1",
1208            "_meta": {"source": "replay", "opaque": {"sequence": 1}}
1209        });
1210        let mut stored: ToolCallUpdate = from_value(initial.clone()).unwrap();
1211        // A metadata-only first update must not invent content or a status.
1212        assert_eq!(to_value(&stored).unwrap(), initial);
1213
1214        let populated = json!({
1215            "toolCallId": "tc_1",
1216            "name": "read_file",
1217            "title": "Reading configuration",
1218            "kind": "read",
1219            "status": "in_progress",
1220            "content": [{
1221                "type": "content",
1222                "content": {
1223                    "type": "text",
1224                    "text": "old",
1225                    "_meta": {"source": "tool"}
1226                }
1227            }],
1228            "locations": [{"path": "/workspace/config.json", "line": 3}],
1229            "rawInput": {"path": "/workspace/config.json"},
1230            "rawOutput": {"text": "old"}
1231        });
1232        stored.apply_update(from_value(populated.clone()).unwrap());
1233        let mut expected = populated;
1234        expected["_meta"] = initial["_meta"].clone();
1235        assert_eq!(to_value(&stored).unwrap(), expected);
1236
1237        for (field, empty) in [
1238            ("content", json!([])),
1239            ("locations", json!([])),
1240            ("rawInput", json!({})),
1241            ("rawOutput", json!({})),
1242        ] {
1243            let original = expected[field].clone();
1244            // Empty values replace rather than merge; null remains distinct.
1245            // Restoring the value also checks that clearing is not permanent.
1246            for replacement in [empty, json!(null), original] {
1247                stored.apply_update(
1248                    from_value(json!({"toolCallId": "tc_1", (field): replacement})).unwrap(),
1249                );
1250                expected[field] = replacement;
1251                assert_eq!(to_value(&stored).unwrap(), expected, "patching {field}");
1252
1253                stored.apply_update(from_value(json!({"toolCallId": "tc_1"})).unwrap());
1254                assert_eq!(to_value(&stored).unwrap(), expected, "omitting {field}");
1255            }
1256        }
1257    }
1258
1259    #[test]
1260    fn tool_call_wire_patches_preserve_unknown_statuses() {
1261        for status in ["deferred", "_awaiting_review"] {
1262            let mut stored = ToolCallUpdate::new("tc_1").status(ToolCallStatus::InProgress);
1263            stored
1264                .apply_update(from_value(json!({"toolCallId": "tc_1", "status": status})).unwrap());
1265            let expected_status = MaybeUndefined::Value(ToolCallStatus::Other(status.to_owned()));
1266            assert_eq!(stored.status, expected_status);
1267
1268            // An unrelated patch must not reset a future or extension status
1269            // to a known value, including a success or failure state.
1270            stored.apply_update(
1271                from_value(json!({"toolCallId": "tc_1", "title": "Still waiting"})).unwrap(),
1272            );
1273            assert_eq!(stored.status, expected_status);
1274            assert_eq!(
1275                to_value(&stored).unwrap(),
1276                json!({"toolCallId": "tc_1", "title": "Still waiting", "status": status})
1277            );
1278        }
1279    }
1280
1281    #[test]
1282    fn tool_call_update_skips_malformed_list_items() {
1283        let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1284            "toolCallId": "tc_1",
1285            "content": [
1286                {
1287                    "type": "content",
1288                    "content": {
1289                        "type": "text",
1290                        "text": "ok"
1291                    }
1292                },
1293                {
1294                    "type": "diff",
1295                    "path": "/bad"
1296                }
1297            ],
1298            "locations": [
1299                {
1300                    "path": "/ok",
1301                    "line": 3
1302                },
1303                {
1304                    "line": 4
1305                }
1306            ]
1307        }))
1308        .unwrap();
1309
1310        let MaybeUndefined::Value(content) = deserialized.content else {
1311            panic!("content should deserialize to a value");
1312        };
1313        assert_eq!(content.len(), 1);
1314
1315        let MaybeUndefined::Value(locations) = deserialized.locations else {
1316            panic!("locations should deserialize to a value");
1317        };
1318        assert_eq!(locations.len(), 1);
1319    }
1320
1321    #[test]
1322    fn tool_call_content_chunk_serializes_single_content_item() {
1323        let chunk = ToolCallContentChunk::new(
1324            "tc_1",
1325            ContentBlock::Text(crate::v2::TextContent::new("partial output")),
1326        );
1327
1328        assert_eq!(
1329            serde_json::to_value(chunk).unwrap(),
1330            serde_json::json!({
1331                "toolCallId": "tc_1",
1332                "content": {
1333                    "type": "content",
1334                    "content": {
1335                        "type": "text",
1336                        "text": "partial output"
1337                    }
1338                }
1339            })
1340        );
1341    }
1342
1343    #[test]
1344    fn terminal_content_serializes_as_display_reference() {
1345        let terminal = ToolCallContent::from(Terminal::new("term_1"));
1346
1347        assert_eq!(
1348            serde_json::to_value(terminal).unwrap(),
1349            serde_json::json!({
1350                "type": "terminal",
1351                "terminalId": "term_1"
1352            })
1353        );
1354    }
1355
1356    #[test]
1357    fn diff_patch_serializes_git_patch_with_structured_changes() {
1358        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";
1359        let diff = ToolCallContent::Diff(Diff::patch(
1360            patch_text,
1361            vec![
1362                DiffChange::modify("/repo/config.json")
1363                    .file_type(DiffFileType::Text)
1364                    .mime_type("application/json"),
1365            ],
1366        ));
1367
1368        assert_eq!(
1369            serde_json::to_value(diff).unwrap(),
1370            serde_json::json!({
1371                "type": "diff",
1372                "changes": [
1373                    {
1374                        "operation": "modify",
1375                        "path": "/repo/config.json",
1376                        "fileType": "text",
1377                        "mimeType": "application/json"
1378                    }
1379                ],
1380                "patch": {
1381                    "format": "git_patch",
1382                    "text": patch_text
1383                }
1384            })
1385        );
1386    }
1387
1388    #[test]
1389    fn diff_patch_requires_text() {
1390        let result = serde_json::from_value::<DiffPatch>(serde_json::json!({
1391            "format": "git_patch",
1392            "diff": "diff --git /repo/config.json /repo/config.json\n"
1393        }));
1394
1395        assert!(result.is_err());
1396    }
1397
1398    #[test]
1399    fn diff_serializes_binary_modify_without_patch_text() {
1400        let diff = ToolCallContent::Diff(Diff::new(vec![
1401            DiffChange::modify("/repo/assets/logo.png")
1402                .file_type(DiffFileType::Binary)
1403                .mime_type("image/png"),
1404        ]));
1405
1406        assert_eq!(
1407            serde_json::to_value(diff).unwrap(),
1408            serde_json::json!({
1409                "type": "diff",
1410                "changes": [
1411                    {
1412                        "operation": "modify",
1413                        "path": "/repo/assets/logo.png",
1414                        "fileType": "binary",
1415                        "mimeType": "image/png"
1416                    }
1417                ]
1418            })
1419        );
1420    }
1421
1422    #[test]
1423    fn diff_move_serializes_shared_fields_with_operation_payload() {
1424        let diff = ToolCallContent::Diff(Diff::new(vec![
1425            DiffChange::move_file("/repo/src/old.rs", "/repo/src/new.rs")
1426                .file_type(DiffFileType::Text)
1427                .mime_type("text/rust"),
1428        ]));
1429
1430        assert_eq!(
1431            serde_json::to_value(diff).unwrap(),
1432            serde_json::json!({
1433                "type": "diff",
1434                "changes": [
1435                    {
1436                        "operation": "move",
1437                        "oldPath": "/repo/src/old.rs",
1438                        "path": "/repo/src/new.rs",
1439                        "fileType": "text",
1440                        "mimeType": "text/rust"
1441                    }
1442                ]
1443            })
1444        );
1445    }
1446
1447    #[test]
1448    fn diff_changes_skip_malformed_list_items() {
1449        let patch_text = "diff --git /ok /ok\ndeleted file mode 100644\n--- /ok\n+++ /dev/null\n@@ -1 +0,0 @@\n-old\n";
1450        let content: ToolCallContent = serde_json::from_value(serde_json::json!({
1451            "type": "diff",
1452            "changes": [
1453                {
1454                    "operation": "modify"
1455                },
1456                {
1457                    "operation": "delete",
1458                    "path": "/ok"
1459                }
1460            ],
1461            "patch": {
1462                "format": "git_patch",
1463                "text": patch_text
1464            }
1465        }))
1466        .unwrap();
1467
1468        let ToolCallContent::Diff(diff) = content else {
1469            panic!("expected diff content");
1470        };
1471        assert_eq!(diff.changes, vec![DiffChange::delete("/ok")]);
1472        assert_eq!(diff.patch, Some(DiffPatch::new(patch_text)));
1473    }
1474
1475    #[test]
1476    fn tool_kind_preserves_unknown_variant() {
1477        let kind: ToolKind = serde_json::from_str("\"review\"").unwrap();
1478        assert_eq!(kind, ToolKind::Unknown("review".to_string()));
1479        assert_eq!(serde_json::to_value(&kind).unwrap(), "review");
1480    }
1481
1482    #[test]
1483    fn tool_call_status_preserves_unknown_variant() {
1484        let status: ToolCallStatus = serde_json::from_str("\"deferred\"").unwrap();
1485        assert_eq!(status, ToolCallStatus::Other("deferred".to_string()));
1486        assert_eq!(serde_json::to_value(&status).unwrap(), "deferred");
1487    }
1488
1489    #[test]
1490    fn tool_call_status_recognizes_cancelled_variant() {
1491        let status: ToolCallStatus = serde_json::from_str("\"cancelled\"").unwrap();
1492        assert_eq!(status, ToolCallStatus::Cancelled);
1493        assert_eq!(serde_json::to_value(&status).unwrap(), "cancelled");
1494    }
1495
1496    #[test]
1497    fn tool_call_content_preserves_unknown_variant() {
1498        let content: ToolCallContent = serde_json::from_value(serde_json::json!({
1499            "type": "_chart",
1500            "title": "Tests",
1501            "data": [1, 2, 3]
1502        }))
1503        .unwrap();
1504
1505        let ToolCallContent::Other(unknown) = content else {
1506            panic!("expected unknown tool call content");
1507        };
1508
1509        assert_eq!(unknown.type_, "_chart");
1510        assert_eq!(
1511            unknown.fields.get("title"),
1512            Some(&serde_json::json!("Tests"))
1513        );
1514        assert_eq!(
1515            serde_json::to_value(ToolCallContent::Other(unknown)).unwrap(),
1516            serde_json::json!({
1517                "type": "_chart",
1518                "title": "Tests",
1519                "data": [1, 2, 3]
1520            })
1521        );
1522    }
1523
1524    #[test]
1525    fn tool_call_content_does_not_hide_malformed_known_variant() {
1526        assert!(
1527            serde_json::from_value::<ToolCallContent>(serde_json::json!({
1528                "type": "diff"
1529            }))
1530            .is_err()
1531        );
1532        assert!(
1533            serde_json::from_value::<ToolCallContent>(serde_json::json!({
1534                "type": "terminal"
1535            }))
1536            .is_err()
1537        );
1538    }
1539}