Skip to main content

agent_client_protocol_schema/v2/
tool_call.rs

1//! Tool calls represent actions that language models request agents to perform.
2//!
3//! When an LLM determines it needs to interact with external systems—like reading files,
4//! running code, or fetching data—it generates tool calls that the agent executes on its behalf.
5//!
6/// See protocol docs: [Tool Calls](https://agentclientprotocol.com/protocol/tool-calls)
7use std::{collections::BTreeMap, sync::Arc};
8
9use derive_more::{Display, From};
10use schemars::{JsonSchema, Schema};
11use serde::{Deserialize, Serialize};
12use serde_with::{DefaultOnError, VecSkipError, serde_as, skip_serializing_none};
13
14use super::{AbsolutePath, ContentBlock, MediaType, Meta, Terminal};
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(forward)]
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<Self>) -> Self {
264        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 standard
345/// content blocks (text, images), file diffs, or display-only terminals.
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#[non_exhaustive]
351pub enum ToolCallContent {
352    /// Standard content block (text, images, resources).
353    Content(Box<Content>),
354    /// File modification shown as a diff.
355    Diff(Diff),
356    /// A display-only reference to an agent-owned terminal.
357    Terminal(Terminal),
358    /// Custom or future tool call content.
359    ///
360    /// Values beginning with `_` are reserved for implementation-specific
361    /// extensions. Unknown values that do not begin with `_` are reserved for
362    /// future ACP variants.
363    ///
364    /// Receivers that do not understand this content type should preserve the
365    /// raw payload when storing, replaying, proxying, or forwarding tool call
366    /// output, and otherwise ignore it or display it generically.
367    #[serde(untagged)]
368    Other(OtherToolCallContent),
369}
370
371/// Custom or future tool call content payload.
372#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
373#[schemars(inline)]
374#[schemars(transform = other_tool_call_content_schema)]
375#[serde(rename_all = "camelCase")]
376#[non_exhaustive]
377pub struct OtherToolCallContent {
378    /// Custom or future tool call content type.
379    ///
380    /// Values beginning with `_` are reserved for implementation-specific
381    /// extensions. Unknown values that do not begin with `_` are reserved for
382    /// future ACP variants.
383    #[serde(rename = "type")]
384    pub type_: String,
385    /// Additional fields from the unknown tool call content payload.
386    #[serde(flatten)]
387    pub fields: BTreeMap<String, serde_json::Value>,
388}
389
390impl OtherToolCallContent {
391    /// Builds [`OtherToolCallContent`] from an unknown discriminator and preserves the remaining extension fields.
392    #[must_use]
393    pub fn new(type_: impl Into<String>, mut fields: BTreeMap<String, serde_json::Value>) -> Self {
394        fields.remove("type");
395        Self {
396            type_: type_.into(),
397            fields,
398        }
399    }
400}
401
402impl<'de> Deserialize<'de> for OtherToolCallContent {
403    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
404    where
405        D: serde::Deserializer<'de>,
406    {
407        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
408        let type_ = fields
409            .remove("type")
410            .ok_or_else(|| serde::de::Error::missing_field("type"))?;
411        let serde_json::Value::String(type_) = type_ else {
412            return Err(serde::de::Error::custom("`type` must be a string"));
413        };
414
415        if is_known_tool_call_content_type(&type_) {
416            return Err(serde::de::Error::custom(format!(
417                "known tool call content `{type_}` did not match its schema"
418            )));
419        }
420
421        Ok(Self { type_, fields })
422    }
423}
424
425fn is_known_tool_call_content_type(type_: &str) -> bool {
426    matches!(type_, "content" | "diff" | "terminal")
427}
428
429fn other_tool_call_content_schema(schema: &mut Schema) {
430    super::schema_util::reject_known_string_discriminators(
431        schema,
432        "type",
433        &["content", "diff", "terminal"],
434    );
435}
436
437impl<T: Into<ContentBlock>> From<T> for ToolCallContent {
438    fn from(content: T) -> Self {
439        ToolCallContent::Content(Box::new(Content::new(content)))
440    }
441}
442
443impl From<Diff> for ToolCallContent {
444    fn from(diff: Diff) -> Self {
445        ToolCallContent::Diff(diff)
446    }
447}
448
449impl From<Terminal> for ToolCallContent {
450    fn from(terminal: Terminal) -> Self {
451        ToolCallContent::Terminal(terminal)
452    }
453}
454
455/// Standard content block (text, images, resources).
456#[serde_as]
457#[skip_serializing_none]
458#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
459#[serde(rename_all = "camelCase")]
460#[non_exhaustive]
461pub struct Content {
462    /// The actual content block.
463    pub content: ContentBlock,
464    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
465    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
466    /// these keys.
467    ///
468    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
469    #[serde_as(deserialize_as = "DefaultOnError")]
470    #[schemars(extend("x-deserialize-default-on-error" = true))]
471    #[serde(default)]
472    #[serde(rename = "_meta")]
473    pub meta: Option<Meta>,
474}
475
476impl Content {
477    /// Builds [`Content`] with the required fields set; optional fields start unset or empty.
478    #[must_use]
479    pub fn new(content: impl Into<ContentBlock>) -> Self {
480        Self {
481            content: content.into(),
482            meta: None,
483        }
484    }
485
486    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
487    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
488    /// these keys.
489    ///
490    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
491    #[must_use]
492    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
493        self.meta = meta.into_option();
494        self
495    }
496}
497
498/// File changes produced by a tool call.
499///
500/// `changes` is authoritative for affected absolute paths and operations.
501/// `patch` optionally carries renderable text for some or all of those changes
502/// and MUST be consistent with `changes`. Agents SHOULD provide `patch` whenever
503/// feasible. Clients MUST handle diffs where `patch` is omitted or `null`.
504///
505/// See protocol docs: [Content](https://agentclientprotocol.com/protocol/tool-calls#content)
506#[serde_as]
507#[skip_serializing_none]
508#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
509#[serde(rename_all = "camelCase")]
510#[non_exhaustive]
511pub struct Diff {
512    /// Structured file changes described by this diff.
513    ///
514    /// Clients can use this field without parsing patch text to determine affected paths.
515    #[serde_as(deserialize_as = "VecSkipError<_, SkipListener>")]
516    #[schemars(extend("x-deserialize-skip-invalid-items" = true))]
517    pub changes: Vec<DiffChange>,
518    /// Renderable patch text for some or all of the structured changes.
519    ///
520    /// Agents SHOULD provide patch text whenever feasible. Omitted or `null`
521    /// means no renderable patch text was provided.
522    #[serde_as(deserialize_as = "DefaultOnError")]
523    #[schemars(extend("x-deserialize-default-on-error" = true))]
524    #[serde(default)]
525    pub patch: Option<DiffPatch>,
526    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
527    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
528    /// these keys.
529    ///
530    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
531    #[serde_as(deserialize_as = "DefaultOnError")]
532    #[schemars(extend("x-deserialize-default-on-error" = true))]
533    #[serde(default)]
534    #[serde(rename = "_meta")]
535    pub meta: Option<Meta>,
536}
537
538impl Diff {
539    /// Builds [`Diff`] with structured file changes.
540    #[must_use]
541    pub fn new(changes: Vec<DiffChange>) -> Self {
542        Self {
543            changes,
544            patch: None,
545            meta: None,
546        }
547    }
548
549    /// Builds [`Diff`] with Git `--patch` text and structured file changes.
550    #[must_use]
551    pub fn patch(text: impl Into<String>, changes: Vec<DiffChange>) -> Self {
552        Self::new(changes).with_patch(DiffPatch::new(text))
553    }
554
555    /// Sets renderable patch text.
556    #[must_use]
557    pub fn with_patch(mut self, patch: impl IntoOption<DiffPatch>) -> Self {
558        self.patch = patch.into_option();
559        self
560    }
561
562    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
563    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
564    /// these keys.
565    ///
566    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
567    #[must_use]
568    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
569        self.meta = meta.into_option();
570        self
571    }
572}
573
574/// Renderable patch text and its format.
575#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
576#[serde(rename_all = "camelCase")]
577#[non_exhaustive]
578pub struct DiffPatch {
579    /// Patch format. The only ACP-defined value is `git_patch`.
580    pub format: DiffPatchFormat,
581    /// Patch text in the format named by `format`.
582    pub text: String,
583}
584
585impl DiffPatch {
586    /// Builds [`DiffPatch`] with Git `--patch` text.
587    #[must_use]
588    pub fn new(text: impl Into<String>) -> Self {
589        Self {
590            format: DiffPatchFormat::GitPatch,
591            text: text.into(),
592        }
593    }
594}
595
596/// Text patch format used by [`DiffPatch`].
597#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
598#[serde(rename_all = "snake_case")]
599#[non_exhaustive]
600pub enum DiffPatchFormat {
601    /// One or more `diff --git` sections in Git's `--patch` (`-p`) text format.
602    ///
603    /// Paths MUST be absolute. Surrounding commit metadata and email envelopes
604    /// MUST NOT be included.
605    GitPatch,
606    /// Custom or future patch format.
607    ///
608    /// Values beginning with `_` are reserved for implementation-specific
609    /// extensions. Unknown values that do not begin with `_` are reserved for
610    /// future ACP variants.
611    #[serde(untagged)]
612    Other(String),
613}
614
615/// Kind of file content represented by a diff change.
616#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
617#[serde(rename_all = "snake_case")]
618#[non_exhaustive]
619pub enum DiffFileType {
620    /// Text content.
621    Text,
622    /// Binary or otherwise non-text content.
623    Binary,
624    /// Directory entry.
625    Directory,
626    /// Symbolic link.
627    Symlink,
628    /// Custom or future file type.
629    ///
630    /// Values beginning with `_` are reserved for implementation-specific
631    /// extensions. Unknown values that do not begin with `_` are reserved for
632    /// future ACP variants.
633    #[serde(untagged)]
634    Other(String),
635}
636
637/// One file-level change described by a [`Diff`].
638///
639/// Structured change metadata lets clients identify affected files and
640/// operations without parsing the text patch.
641#[serde_as]
642#[skip_serializing_none]
643#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
644#[serde(rename_all = "camelCase")]
645#[non_exhaustive]
646pub struct DiffChange {
647    /// File content kind.
648    ///
649    /// Omitted or `null` means the content kind is unknown.
650    #[serde_as(deserialize_as = "DefaultOnError")]
651    #[schemars(extend("x-deserialize-default-on-error" = true))]
652    #[serde(default)]
653    pub file_type: Option<DiffFileType>,
654    /// MIME type of the file contents.
655    ///
656    /// Omitted or `null` means the MIME type is unknown.
657    #[serde_as(deserialize_as = "DefaultOnError")]
658    #[schemars(extend("x-deserialize-default-on-error" = true))]
659    #[serde(default)]
660    pub mime_type: Option<MediaType>,
661    /// File operation-specific fields.
662    #[serde(flatten)]
663    pub operation: DiffChangeOperation,
664    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
665    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
666    /// these keys.
667    ///
668    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
669    #[serde_as(deserialize_as = "DefaultOnError")]
670    #[schemars(extend("x-deserialize-default-on-error" = true))]
671    #[serde(default)]
672    #[serde(rename = "_meta")]
673    pub meta: Option<Meta>,
674}
675
676impl DiffChange {
677    /// Builds [`DiffChange`] with the required fields set; optional fields start unset or empty.
678    #[must_use]
679    pub fn new(operation: DiffChangeOperation) -> Self {
680        Self {
681            file_type: None,
682            mime_type: None,
683            operation,
684            meta: None,
685        }
686    }
687
688    /// Builds a file add change.
689    #[must_use]
690    pub fn add(path: impl Into<AbsolutePath>) -> Self {
691        Self::new(DiffChangeOperation::Add(DiffPathChange::new(path)))
692    }
693
694    /// Builds a file delete change.
695    #[must_use]
696    pub fn delete(path: impl Into<AbsolutePath>) -> Self {
697        Self::new(DiffChangeOperation::Delete(DiffPathChange::new(path)))
698    }
699
700    /// Builds a file modify change.
701    #[must_use]
702    pub fn modify(path: impl Into<AbsolutePath>) -> Self {
703        Self::new(DiffChangeOperation::Modify(DiffPathChange::new(path)))
704    }
705
706    /// Builds a file move or rename change.
707    #[must_use]
708    pub fn move_file(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
709        Self::new(DiffChangeOperation::Move(DiffPathPairChange::new(
710            old_path, path,
711        )))
712    }
713
714    /// Builds a file copy change.
715    #[must_use]
716    pub fn copy(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
717        Self::new(DiffChangeOperation::Copy(DiffPathPairChange::new(
718            old_path, path,
719        )))
720    }
721
722    /// File content kind.
723    ///
724    /// Omitted or `null` means the content kind is unknown.
725    #[must_use]
726    pub fn file_type(mut self, file_type: impl IntoOption<DiffFileType>) -> Self {
727        self.file_type = file_type.into_option();
728        self
729    }
730
731    /// MIME type of the file contents.
732    ///
733    /// Omitted or `null` means the MIME type is unknown.
734    #[must_use]
735    pub fn mime_type(mut self, mime_type: impl IntoOption<MediaType>) -> Self {
736        self.mime_type = mime_type.into_option();
737        self
738    }
739
740    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
741    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
742    /// these keys.
743    ///
744    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
745    #[must_use]
746    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
747        self.meta = meta.into_option();
748        self
749    }
750}
751
752/// File operation for a [`DiffChange`].
753#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
754#[serde(tag = "operation", rename_all = "snake_case")]
755#[non_exhaustive]
756pub enum DiffChangeOperation {
757    /// A file was added.
758    Add(DiffPathChange),
759    /// A file was deleted.
760    Delete(DiffPathChange),
761    /// A file was modified in place.
762    Modify(DiffPathChange),
763    /// A file was moved or renamed.
764    Move(DiffPathPairChange),
765    /// A file was copied.
766    Copy(DiffPathPairChange),
767    /// Custom or future file operation.
768    ///
769    /// Values beginning with `_` are reserved for implementation-specific
770    /// extensions. Unknown values that do not begin with `_` are reserved for
771    /// future ACP variants.
772    #[serde(untagged)]
773    Other(OtherDiffChange),
774}
775
776/// Operation metadata for add, delete, and modify changes.
777#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
778#[serde(rename_all = "camelCase")]
779#[non_exhaustive]
780pub struct DiffPathChange {
781    /// Absolute path for the operation.
782    pub path: AbsolutePath,
783}
784
785impl DiffPathChange {
786    /// Builds [`DiffPathChange`] with the required fields set; optional fields start unset or empty.
787    #[must_use]
788    pub fn new(path: impl Into<AbsolutePath>) -> Self {
789        Self { path: path.into() }
790    }
791}
792
793/// Operation metadata for move and copy changes.
794#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
795#[serde(rename_all = "camelCase")]
796#[non_exhaustive]
797pub struct DiffPathPairChange {
798    /// Absolute path before the operation.
799    pub old_path: AbsolutePath,
800    /// Absolute path after the operation.
801    pub path: AbsolutePath,
802}
803
804impl DiffPathPairChange {
805    /// Builds [`DiffPathPairChange`] with the required fields set; optional fields start unset or empty.
806    #[must_use]
807    pub fn new(old_path: impl Into<AbsolutePath>, path: impl Into<AbsolutePath>) -> Self {
808        Self {
809            old_path: old_path.into(),
810            path: path.into(),
811        }
812    }
813}
814
815/// Custom or future file operation payload.
816#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
817#[schemars(inline)]
818#[schemars(transform = other_diff_change_schema)]
819#[serde(rename_all = "camelCase")]
820#[non_exhaustive]
821pub struct OtherDiffChange {
822    /// Custom or future file operation.
823    ///
824    /// Values beginning with `_` are reserved for implementation-specific
825    /// extensions. Unknown values that do not begin with `_` are reserved for
826    /// future ACP variants.
827    pub operation: String,
828    /// Additional fields from the unknown file operation payload.
829    #[serde(flatten)]
830    pub fields: BTreeMap<String, serde_json::Value>,
831}
832
833impl OtherDiffChange {
834    /// Builds [`OtherDiffChange`] from an unknown discriminator and preserves the remaining extension fields.
835    #[must_use]
836    pub fn new(
837        operation: impl Into<String>,
838        mut fields: BTreeMap<String, serde_json::Value>,
839    ) -> Self {
840        fields.remove("operation");
841        fields.remove("fileType");
842        fields.remove("mimeType");
843        fields.remove("_meta");
844        Self {
845            operation: operation.into(),
846            fields,
847        }
848    }
849}
850
851impl<'de> Deserialize<'de> for OtherDiffChange {
852    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
853    where
854        D: serde::Deserializer<'de>,
855    {
856        let mut fields = BTreeMap::<String, serde_json::Value>::deserialize(deserializer)?;
857        let operation = fields
858            .remove("operation")
859            .ok_or_else(|| serde::de::Error::missing_field("operation"))?;
860        let serde_json::Value::String(operation) = operation else {
861            return Err(serde::de::Error::custom("`operation` must be a string"));
862        };
863
864        if is_known_diff_change_operation(&operation) {
865            return Err(serde::de::Error::custom(format!(
866                "known diff change operation `{operation}` did not match its schema"
867            )));
868        }
869        fields.remove("fileType");
870        fields.remove("mimeType");
871        fields.remove("_meta");
872
873        Ok(Self { operation, fields })
874    }
875}
876
877fn is_known_diff_change_operation(operation: &str) -> bool {
878    matches!(operation, "add" | "delete" | "modify" | "move" | "copy")
879}
880
881fn other_diff_change_schema(schema: &mut Schema) {
882    super::schema_util::reject_known_string_discriminators(
883        schema,
884        "operation",
885        &["add", "delete", "modify", "move", "copy"],
886    );
887}
888
889/// A file location being accessed or modified by a tool.
890///
891/// Enables clients to implement "follow-along" features that track
892/// which files the agent is working with in real-time.
893///
894/// See protocol docs: [Following the Agent](https://agentclientprotocol.com/protocol/tool-calls#following-the-agent)
895#[serde_as]
896#[skip_serializing_none]
897#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
898#[serde(rename_all = "camelCase")]
899#[non_exhaustive]
900pub struct ToolCallLocation {
901    /// The absolute file path being accessed or modified.
902    pub path: AbsolutePath,
903    /// Optional line number within the file.
904    #[serde_as(deserialize_as = "DefaultOnError")]
905    #[schemars(extend("x-deserialize-default-on-error" = true))]
906    #[serde(default)]
907    pub line: Option<u32>,
908    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
909    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
910    /// these keys.
911    ///
912    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
913    #[serde_as(deserialize_as = "DefaultOnError")]
914    #[schemars(extend("x-deserialize-default-on-error" = true))]
915    #[serde(default)]
916    #[serde(rename = "_meta")]
917    pub meta: Option<Meta>,
918}
919
920impl ToolCallLocation {
921    /// Builds [`ToolCallLocation`] with the required fields set; optional fields start unset or empty.
922    #[must_use]
923    pub fn new(path: impl Into<AbsolutePath>) -> Self {
924        Self {
925            path: path.into(),
926            line: None,
927            meta: None,
928        }
929    }
930
931    /// Optional line number within the file.
932    #[must_use]
933    pub fn line(mut self, line: impl IntoOption<u32>) -> Self {
934        self.line = line.into_option();
935        self
936    }
937
938    /// The _meta property is reserved by ACP to allow clients and agents to attach additional
939    /// metadata to their interactions. Implementations MUST NOT make assumptions about values at
940    /// these keys.
941    ///
942    /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
943    #[must_use]
944    pub fn meta(mut self, meta: impl IntoOption<Meta>) -> Self {
945        self.meta = meta.into_option();
946        self
947    }
948}
949
950#[cfg(test)]
951mod tests {
952    use super::*;
953    use crate::MaybeUndefined;
954
955    #[test]
956    fn tool_call_serializes_as_upsert() {
957        let tool_call = ToolCallUpdate::new("tc_1")
958            .title("Reading configuration")
959            .status(ToolCallStatus::InProgress)
960            .raw_input(serde_json::json!({"path": "settings.json"}));
961
962        assert_eq!(
963            serde_json::to_value(tool_call).unwrap(),
964            serde_json::json!({
965                "toolCallId": "tc_1",
966                "title": "Reading configuration",
967                "status": "in_progress",
968                "rawInput": {
969                    "path": "settings.json"
970                }
971            })
972        );
973    }
974
975    #[test]
976    fn tool_call_update_distinguishes_omitted_null_and_value() {
977        let tool_call = ToolCallUpdate::new("tc_1")
978            .status(ToolCallStatus::Completed)
979            .content(None::<Vec<ToolCallContent>>);
980
981        assert_eq!(
982            serde_json::to_value(tool_call).unwrap(),
983            serde_json::json!({
984                "toolCallId": "tc_1",
985                "status": "completed",
986                "content": null
987            })
988        );
989
990        let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
991            "toolCallId": "tc_1",
992            "status": null,
993            "locations": []
994        }))
995        .unwrap();
996        assert_eq!(deserialized.title, MaybeUndefined::Undefined);
997        assert_eq!(deserialized.status, MaybeUndefined::Null);
998        assert_eq!(deserialized.locations, MaybeUndefined::Value(Vec::new()));
999    }
1000
1001    #[test]
1002    fn tool_call_update_distinguishes_meta_omitted_null_and_value() {
1003        let mut meta = Meta::new();
1004        meta.insert("source".to_string(), serde_json::json!("tool-call"));
1005
1006        assert_eq!(
1007            serde_json::to_value(ToolCallUpdate::new("tc_1").meta(meta.clone())).unwrap(),
1008            serde_json::json!({
1009                "toolCallId": "tc_1",
1010                "_meta": {
1011                    "source": "tool-call"
1012                }
1013            })
1014        );
1015
1016        assert_eq!(
1017            serde_json::to_value(ToolCallUpdate::new("tc_1").meta(None::<Meta>)).unwrap(),
1018            serde_json::json!({
1019                "toolCallId": "tc_1",
1020                "_meta": null
1021            })
1022        );
1023
1024        let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1025            "toolCallId": "tc_1",
1026            "_meta": null
1027        }))
1028        .unwrap();
1029        assert_eq!(deserialized.meta, MaybeUndefined::Null);
1030
1031        let patch = ToolCallUpdate::new("tc_1");
1032        assert_eq!(patch.meta, MaybeUndefined::Undefined);
1033
1034        let mut stored = ToolCallUpdate::new("tc_1").meta(meta);
1035        stored.apply_update(ToolCallUpdate::new("tc_1").meta(None::<Meta>));
1036        assert_eq!(stored.meta, MaybeUndefined::Null);
1037    }
1038
1039    #[test]
1040    fn tool_call_update_skips_malformed_list_items() {
1041        let deserialized: ToolCallUpdate = serde_json::from_value(serde_json::json!({
1042            "toolCallId": "tc_1",
1043            "content": [
1044                {
1045                    "type": "content",
1046                    "content": {
1047                        "type": "text",
1048                        "text": "ok"
1049                    }
1050                },
1051                {
1052                    "type": "diff",
1053                    "path": "/bad"
1054                }
1055            ],
1056            "locations": [
1057                {
1058                    "path": "/ok",
1059                    "line": 3
1060                },
1061                {
1062                    "line": 4
1063                }
1064            ]
1065        }))
1066        .unwrap();
1067
1068        let MaybeUndefined::Value(content) = deserialized.content else {
1069            panic!("content should deserialize to a value");
1070        };
1071        assert_eq!(content.len(), 1);
1072
1073        let MaybeUndefined::Value(locations) = deserialized.locations else {
1074            panic!("locations should deserialize to a value");
1075        };
1076        assert_eq!(locations.len(), 1);
1077    }
1078
1079    #[test]
1080    fn tool_call_content_chunk_serializes_single_content_item() {
1081        let chunk = ToolCallContentChunk::new(
1082            "tc_1",
1083            ContentBlock::Text(crate::v2::TextContent::new("partial output")),
1084        );
1085
1086        assert_eq!(
1087            serde_json::to_value(chunk).unwrap(),
1088            serde_json::json!({
1089                "toolCallId": "tc_1",
1090                "content": {
1091                    "type": "content",
1092                    "content": {
1093                        "type": "text",
1094                        "text": "partial output"
1095                    }
1096                }
1097            })
1098        );
1099    }
1100
1101    #[test]
1102    fn terminal_content_serializes_as_display_reference() {
1103        let terminal = ToolCallContent::from(Terminal::new("term_1"));
1104
1105        assert_eq!(
1106            serde_json::to_value(terminal).unwrap(),
1107            serde_json::json!({
1108                "type": "terminal",
1109                "terminalId": "term_1"
1110            })
1111        );
1112    }
1113
1114    #[test]
1115    fn diff_patch_serializes_git_patch_with_structured_changes() {
1116        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";
1117        let diff = ToolCallContent::Diff(Diff::patch(
1118            patch_text,
1119            vec![
1120                DiffChange::modify("/repo/config.json")
1121                    .file_type(DiffFileType::Text)
1122                    .mime_type("application/json"),
1123            ],
1124        ));
1125
1126        assert_eq!(
1127            serde_json::to_value(diff).unwrap(),
1128            serde_json::json!({
1129                "type": "diff",
1130                "changes": [
1131                    {
1132                        "operation": "modify",
1133                        "path": "/repo/config.json",
1134                        "fileType": "text",
1135                        "mimeType": "application/json"
1136                    }
1137                ],
1138                "patch": {
1139                    "format": "git_patch",
1140                    "text": patch_text
1141                }
1142            })
1143        );
1144    }
1145
1146    #[test]
1147    fn diff_patch_requires_text() {
1148        let result = serde_json::from_value::<DiffPatch>(serde_json::json!({
1149            "format": "git_patch",
1150            "diff": "diff --git /repo/config.json /repo/config.json\n"
1151        }));
1152
1153        assert!(result.is_err());
1154    }
1155
1156    #[test]
1157    fn diff_serializes_binary_modify_without_patch_text() {
1158        let diff = ToolCallContent::Diff(Diff::new(vec![
1159            DiffChange::modify("/repo/assets/logo.png")
1160                .file_type(DiffFileType::Binary)
1161                .mime_type("image/png"),
1162        ]));
1163
1164        assert_eq!(
1165            serde_json::to_value(diff).unwrap(),
1166            serde_json::json!({
1167                "type": "diff",
1168                "changes": [
1169                    {
1170                        "operation": "modify",
1171                        "path": "/repo/assets/logo.png",
1172                        "fileType": "binary",
1173                        "mimeType": "image/png"
1174                    }
1175                ]
1176            })
1177        );
1178    }
1179
1180    #[test]
1181    fn diff_move_serializes_shared_fields_with_operation_payload() {
1182        let diff = ToolCallContent::Diff(Diff::new(vec![
1183            DiffChange::move_file("/repo/src/old.rs", "/repo/src/new.rs")
1184                .file_type(DiffFileType::Text)
1185                .mime_type("text/rust"),
1186        ]));
1187
1188        assert_eq!(
1189            serde_json::to_value(diff).unwrap(),
1190            serde_json::json!({
1191                "type": "diff",
1192                "changes": [
1193                    {
1194                        "operation": "move",
1195                        "oldPath": "/repo/src/old.rs",
1196                        "path": "/repo/src/new.rs",
1197                        "fileType": "text",
1198                        "mimeType": "text/rust"
1199                    }
1200                ]
1201            })
1202        );
1203    }
1204
1205    #[test]
1206    fn diff_changes_skip_malformed_list_items() {
1207        let patch_text = "diff --git /ok /ok\ndeleted file mode 100644\n--- /ok\n+++ /dev/null\n@@ -1 +0,0 @@\n-old\n";
1208        let content: ToolCallContent = serde_json::from_value(serde_json::json!({
1209            "type": "diff",
1210            "changes": [
1211                {
1212                    "operation": "modify"
1213                },
1214                {
1215                    "operation": "delete",
1216                    "path": "/ok"
1217                }
1218            ],
1219            "patch": {
1220                "format": "git_patch",
1221                "text": patch_text
1222            }
1223        }))
1224        .unwrap();
1225
1226        let ToolCallContent::Diff(diff) = content else {
1227            panic!("expected diff content");
1228        };
1229        assert_eq!(diff.changes, vec![DiffChange::delete("/ok")]);
1230        assert_eq!(diff.patch, Some(DiffPatch::new(patch_text)));
1231    }
1232
1233    #[test]
1234    fn tool_kind_preserves_unknown_variant() {
1235        let kind: ToolKind = serde_json::from_str("\"review\"").unwrap();
1236        assert_eq!(kind, ToolKind::Unknown("review".to_string()));
1237        assert_eq!(serde_json::to_value(&kind).unwrap(), "review");
1238    }
1239
1240    #[test]
1241    fn tool_call_status_preserves_unknown_variant() {
1242        let status: ToolCallStatus = serde_json::from_str("\"deferred\"").unwrap();
1243        assert_eq!(status, ToolCallStatus::Other("deferred".to_string()));
1244        assert_eq!(serde_json::to_value(&status).unwrap(), "deferred");
1245    }
1246
1247    #[test]
1248    fn tool_call_content_preserves_unknown_variant() {
1249        let content: ToolCallContent = serde_json::from_value(serde_json::json!({
1250            "type": "_chart",
1251            "title": "Tests",
1252            "data": [1, 2, 3]
1253        }))
1254        .unwrap();
1255
1256        let ToolCallContent::Other(unknown) = content else {
1257            panic!("expected unknown tool call content");
1258        };
1259
1260        assert_eq!(unknown.type_, "_chart");
1261        assert_eq!(
1262            unknown.fields.get("title"),
1263            Some(&serde_json::json!("Tests"))
1264        );
1265        assert_eq!(
1266            serde_json::to_value(ToolCallContent::Other(unknown)).unwrap(),
1267            serde_json::json!({
1268                "type": "_chart",
1269                "title": "Tests",
1270                "data": [1, 2, 3]
1271            })
1272        );
1273    }
1274
1275    #[test]
1276    fn tool_call_content_does_not_hide_malformed_known_variant() {
1277        assert!(
1278            serde_json::from_value::<ToolCallContent>(serde_json::json!({
1279                "type": "diff"
1280            }))
1281            .is_err()
1282        );
1283        assert!(
1284            serde_json::from_value::<ToolCallContent>(serde_json::json!({
1285                "type": "terminal"
1286            }))
1287            .is_err()
1288        );
1289    }
1290}