Skip to main content

agent_client_protocol_schema/v2/
mod.rs

1//! Agent Client Protocol version 2 draft types.
2//!
3//! **EXPERIMENTAL.** This module is gated behind the `unstable_protocol_v2`
4//! feature, is not part of the [`unstable`] umbrella, and must be selected
5//! explicitly with [`crate::ProtocolVersion::V2`]. The types here evolve v2
6//! without disturbing the stable v1 API, and the wire format intentionally
7//! diverges from v1 as draft v2 RFDs land. The type definitions may change at
8//! any time.
9//!
10//! [`unstable`]: https://docs.rs/crate/agent-client-protocol-schema/latest/features
11
12mod agent;
13mod client;
14mod content;
15mod elicitation;
16mod error;
17mod ext;
18#[cfg(feature = "unstable_mcp_over_acp")]
19mod mcp;
20#[cfg(feature = "unstable_nes")]
21mod nes;
22mod plan;
23mod protocol_level;
24#[cfg(feature = "schemars")]
25pub(crate) mod schema_util;
26mod terminal;
27mod tool_call;
28
29pub use crate::rpc::{JsonRpcBatch, JsonRpcMessage, Notification, Request, RequestId};
30pub use agent::*;
31pub use client::*;
32pub use content::*;
33use derive_more::{Display, From};
34pub use elicitation::*;
35pub use error::*;
36pub use ext::*;
37#[cfg(feature = "unstable_mcp_over_acp")]
38pub use mcp::*;
39#[cfg(feature = "unstable_nes")]
40pub use nes::*;
41pub use plan::*;
42pub use protocol_level::*;
43pub use serde_json::value::RawValue;
44pub use terminal::*;
45pub use tool_call::*;
46
47/// JSON-RPC response envelope using this protocol version's error type.
48pub type Response<Result> = crate::rpc::Response<Result, Error>;
49
50use serde::{Deserialize, Serialize};
51use std::{
52    borrow::Cow,
53    ffi::{OsStr, OsString},
54    path::{Path, PathBuf},
55    sync::Arc,
56};
57
58/// An absolute filesystem path used by the protocol.
59#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, From)]
61#[serde(transparent)]
62#[from(forward)]
63#[non_exhaustive]
64pub struct AbsolutePath(pub PathBuf);
65
66impl AbsolutePath {
67    /// Wraps a filesystem path as a typed [`AbsolutePath`].
68    #[must_use]
69    pub fn new(path: impl Into<Self>) -> Self {
70        path.into()
71    }
72
73    /// Returns the wrapped filesystem path.
74    #[must_use]
75    pub fn into_inner(self) -> PathBuf {
76        self.0
77    }
78}
79
80impl AsRef<Path> for AbsolutePath {
81    fn as_ref(&self) -> &Path {
82        self.0.as_path()
83    }
84}
85
86impl AsRef<OsStr> for AbsolutePath {
87    fn as_ref(&self) -> &OsStr {
88        self.0.as_os_str()
89    }
90}
91
92macro_rules! impl_into_option_conversion {
93    ($target:ty, $source:ty) => {
94        impl crate::IntoOption<$target> for $source {
95            fn into_option(self) -> Option<$target> {
96                Some(self.into())
97            }
98        }
99    };
100}
101
102macro_rules! impl_into_maybe_undefined_conversion {
103    ($target:ty, $source:ty) => {
104        impl crate::IntoMaybeUndefined<$target> for $source {
105            fn into_maybe_undefined(self) -> crate::MaybeUndefined<$target> {
106                crate::MaybeUndefined::Value(self.into())
107            }
108        }
109    };
110}
111
112impl_into_option_conversion!(AbsolutePath, PathBuf);
113impl_into_option_conversion!(AbsolutePath, OsString);
114impl_into_option_conversion!(AbsolutePath, String);
115impl_into_option_conversion!(AbsolutePath, Box<Path>);
116impl_into_option_conversion!(AbsolutePath, Cow<'_, Path>);
117impl_into_maybe_undefined_conversion!(AbsolutePath, PathBuf);
118impl_into_maybe_undefined_conversion!(AbsolutePath, OsString);
119impl_into_maybe_undefined_conversion!(AbsolutePath, String);
120impl_into_maybe_undefined_conversion!(AbsolutePath, Box<Path>);
121impl_into_maybe_undefined_conversion!(AbsolutePath, Cow<'_, Path>);
122
123impl<T: ?Sized + AsRef<OsStr>> crate::IntoOption<AbsolutePath> for &T {
124    fn into_option(self) -> Option<AbsolutePath> {
125        Some(self.into())
126    }
127}
128
129impl<T: ?Sized + AsRef<OsStr>> crate::IntoMaybeUndefined<AbsolutePath> for &T {
130    fn into_maybe_undefined(self) -> crate::MaybeUndefined<AbsolutePath> {
131        crate::MaybeUndefined::Value(self.into())
132    }
133}
134/// A unique identifier for a conversation session between a client and agent.
135///
136/// Sessions maintain their own context, conversation history, and state,
137/// allowing multiple independent interactions with the same agent.
138///
139/// See protocol docs: [Session ID](https://agentclientprotocol.com/protocol/session-setup#session-id)
140#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
141#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
142#[serde(transparent)]
143#[from(forward)]
144#[non_exhaustive]
145pub struct SessionId(pub Arc<str>);
146
147impl SessionId {
148    /// Wraps a protocol string as a typed [`SessionId`].
149    #[must_use]
150    pub fn new(id: impl Into<Self>) -> Self {
151        id.into()
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn semantic_newtype_builders_remain_ergonomic() {
161        let request = NewSessionRequest::new("/workspace")
162            .additional_directories(["/workspace/shared", "/workspace/docs"]);
163        assert_eq!(request.cwd, AbsolutePath::new("/workspace"));
164        assert_eq!(
165            <AbsolutePath as AsRef<Path>>::as_ref(&request.cwd),
166            Path::new("/workspace")
167        );
168        assert_eq!(
169            <AbsolutePath as AsRef<OsStr>>::as_ref(&request.cwd),
170            OsStr::new("/workspace")
171        );
172
173        let list = ListSessionsRequest::new()
174            .cwd("/workspace")
175            .cursor("next-page");
176        assert_eq!(list.cursor, Some(SessionListCursor::new("next-page")));
177        assert_eq!(
178            <SessionListCursor as AsRef<str>>::as_ref(list.cursor.as_ref().unwrap()),
179            "next-page"
180        );
181
182        let image = ImageContent::new("aGVsbG8=", "image/png");
183        assert_eq!(image.mime_type, MediaType::new("image/png"));
184        assert_eq!(
185            <MediaType as AsRef<str>>::as_ref(&image.mime_type),
186            "image/png"
187        );
188
189        let session_id_source = String::from("session-1");
190        let session_id = SessionId::new(session_id_source.as_str());
191        assert_eq!(SessionId::new(session_id).to_string(), "session-1");
192
193        let os_path = OsString::from("/workspace");
194        assert_eq!(
195            ListSessionsRequest::new().cwd(&os_path).cwd,
196            Some(AbsolutePath::new(&os_path))
197        );
198        drop(TerminalUpdate::new("terminal-1").cwd(&os_path));
199
200        drop(ListSessionsRequest::new().cwd(None).cursor(None));
201        drop(ListSessionsResponse::new(Vec::new()).next_cursor(None));
202        drop(Icon::new("https://example.com/icon.png").mime_type(None));
203        drop(TerminalUpdate::new("terminal-1").cwd(None));
204    }
205}