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}