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