Skip to main content

agent_client_protocol_schema/v2/
tool_call.rs

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