Skip to main content

agent_client_protocol_schema/v2/
ext.rs

1//! Extension types and constants for protocol extensibility.
2use derive_more::From;
3use serde::{Deserialize, Serialize};
4use serde_json::value::RawValue;
5use std::sync::Arc;
6
7use crate::MaybeUndefined;
8
9/// Value attached to a given ACP type on the `_meta` field.
10///
11/// The _meta property is reserved by ACP to allow clients and agents to attach
12/// additional metadata to their interactions. Implementations MUST NOT make assumptions about
13/// values at these keys.
14///
15/// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
16pub type Meta = serde_json::Map<String, serde_json::Value>;
17
18/// Applies the top-level `_meta` patch of an upsert update to stored metadata.
19///
20/// An omitted `update` leaves `stored` unchanged, and `null` clears it. An
21/// object patches `stored` one top-level key at a time: keys it omits keep
22/// their stored values, a key set to `null` is removed, and any other value
23/// replaces that key's stored value whole. Nested objects are not merged, and
24/// a `null` nested inside a value is kept as data. Removing every key leaves an
25/// empty object, which means the same as no metadata.
26///
27/// Use this for the top-level `_meta` of upsert updates such as
28/// [`ToolCallUpdate`](super::ToolCallUpdate),
29/// [`TerminalUpdate`](super::TerminalUpdate), message updates,
30/// [`SessionInfoUpdate`](super::SessionInfoUpdate), and
31/// [`CompactionUpdate`](super::CompactionUpdate). Other `_meta` fields are not
32/// patches.
33///
34/// See protocol docs: [Metadata Patches](https://agentclientprotocol.com/protocol/extensibility#metadata-patches)
35pub fn apply_meta_update(stored: &mut MaybeUndefined<Meta>, update: MaybeUndefined<Meta>) {
36    match update {
37        MaybeUndefined::Undefined => {}
38        MaybeUndefined::Null => *stored = MaybeUndefined::Null,
39        MaybeUndefined::Value(patch) => {
40            let mut meta = match std::mem::take(stored) {
41                MaybeUndefined::Value(meta) => meta,
42                MaybeUndefined::Undefined | MaybeUndefined::Null => Meta::new(),
43            };
44            for (key, value) in patch {
45                if value.is_null() {
46                    meta.remove(&key);
47                } else {
48                    meta.insert(key, value);
49                }
50            }
51            *stored = MaybeUndefined::Value(meta);
52        }
53    }
54}
55
56/// Allows for sending an arbitrary request that is not part of the ACP spec.
57/// Extension methods provide a way to add custom functionality while maintaining
58/// protocol compatibility.
59///
60/// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
61#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
62#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(transparent)]
64#[non_exhaustive]
65pub struct ExtRequest {
66    /// Wire method name for this extension request.
67    ///
68    /// Extension method names must start with `_`.
69    #[serde(skip)] // this is used for routing, but when serializing we only want the params
70    pub method: Arc<str>,
71    /// Raw JSON parameters for this extension message.
72    #[cfg_attr(feature = "schemars", schemars(with = "serde_json::Value"))]
73    pub params: Arc<RawValue>,
74}
75
76impl ExtRequest {
77    /// Builds [`ExtRequest`] with the required request fields set; optional fields start unset or empty.
78    #[must_use]
79    pub fn new(method: impl Into<Arc<str>>, params: Arc<RawValue>) -> Self {
80        Self {
81            method: method.into(),
82            params,
83        }
84    }
85}
86
87/// Allows for sending an arbitrary response to an [`ExtRequest`] that is not part of the ACP spec.
88/// Extension methods provide a way to add custom functionality while maintaining
89/// protocol compatibility.
90///
91/// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
92#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
93#[derive(Debug, Clone, Serialize, Deserialize, From)]
94#[serde(transparent)]
95#[non_exhaustive]
96pub struct ExtResponse(
97    #[cfg_attr(feature = "schemars", schemars(with = "serde_json::Value"))] pub Arc<RawValue>,
98);
99
100impl ExtResponse {
101    /// Builds [`ExtResponse`] with the required response fields set; optional fields start unset or empty.
102    #[must_use]
103    pub fn new(params: Arc<RawValue>) -> Self {
104        Self(params)
105    }
106}
107
108/// Allows the Agent to send an arbitrary notification that is not part of the ACP spec.
109/// Extension notifications provide a way to send one-way messages for custom functionality
110/// while maintaining protocol compatibility.
111///
112/// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)
113#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
114#[derive(Debug, Clone, Serialize, Deserialize)]
115#[serde(transparent)]
116#[non_exhaustive]
117pub struct ExtNotification {
118    /// Wire method name for this extension notification.
119    ///
120    /// Extension method names must start with `_`.
121    #[serde(skip)] // this is used for routing, but when serializing we only want the params
122    pub method: Arc<str>,
123    /// Raw JSON parameters for this extension message.
124    #[cfg_attr(feature = "schemars", schemars(with = "serde_json::Value"))]
125    pub params: Arc<RawValue>,
126}
127
128impl ExtNotification {
129    /// Builds [`ExtNotification`] with the required notification fields set; optional fields start unset or empty.
130    #[must_use]
131    pub fn new(method: impl Into<Arc<str>>, params: Arc<RawValue>) -> Self {
132        Self {
133            method: method.into(),
134            params,
135        }
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use serde_json::{from_value, json};
142
143    use super::*;
144
145    fn meta(value: serde_json::Value) -> MaybeUndefined<Meta> {
146        from_value(value).unwrap()
147    }
148
149    #[test]
150    fn meta_update_patches_top_level_keys() {
151        let mut stored = meta(json!({
152            "zed.dev/kept": 1,
153            "zed.dev/removed": true,
154            "acme.dev/nested": {"a": 1, "b": 2},
155            "acme.dev/list": [1, 2]
156        }));
157
158        apply_meta_update(
159            &mut stored,
160            meta(json!({
161                "zed.dev/removed": null,
162                "zed.dev/absent": null,
163                "acme.dev/nested": {"b": 3, "c": null},
164                "acme.dev/list": [3],
165                "acme.dev/added": []
166            })),
167        );
168
169        // Values replace the stored key whole instead of merging or appending,
170        // and nested nulls are data rather than removals.
171        assert_eq!(
172            stored,
173            meta(json!({
174                "zed.dev/kept": 1,
175                "acme.dev/nested": {"b": 3, "c": null},
176                "acme.dev/list": [3],
177                "acme.dev/added": []
178            }))
179        );
180    }
181
182    #[test]
183    fn meta_update_omitted_empty_and_null() {
184        let original = meta(json!({"zed.dev/kept": 1}));
185        let mut stored = original.clone();
186
187        apply_meta_update(&mut stored, MaybeUndefined::Undefined);
188        assert_eq!(stored, original);
189
190        // An empty patch changes no keys.
191        apply_meta_update(&mut stored, meta(json!({})));
192        assert_eq!(stored, original);
193
194        apply_meta_update(&mut stored, MaybeUndefined::Null);
195        assert_eq!(stored, MaybeUndefined::Null);
196    }
197
198    #[test]
199    fn meta_update_starts_from_empty_after_clear_or_on_first_update() {
200        for initial in [MaybeUndefined::Undefined, MaybeUndefined::Null] {
201            let mut stored = initial;
202            apply_meta_update(
203                &mut stored,
204                meta(json!({"zed.dev/added": 1, "zed.dev/removed": null})),
205            );
206            // A `null` key value is never stored.
207            assert_eq!(stored, meta(json!({"zed.dev/added": 1})));
208        }
209    }
210}