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;
20mod plan;
21mod protocol_level;
22mod rpc;
23#[cfg(feature = "schemars")]
24pub(crate) mod schema_util;
25mod terminal;
26mod tool_call;
27
28pub use crate::rpc::{JsonRpcMessage, Notification};
29pub use agent::*;
30pub use client::*;
31pub use content::*;
32use derive_more::{Display, From};
33pub use elicitation::*;
34pub use error::*;
35pub use ext::*;
36#[cfg(feature = "unstable_mcp_over_acp")]
37pub use mcp::*;
38pub use plan::*;
39pub use protocol_level::*;
40pub use rpc::*;
41pub use serde_json::value::RawValue;
42pub use terminal::*;
43pub use tool_call::*;
44
45use serde::{Deserialize, Serialize};
46use std::{
47    borrow::Cow,
48    ffi::{OsStr, OsString},
49    path::{Path, PathBuf},
50    sync::Arc,
51};
52
53/// An absolute filesystem path used by the protocol.
54///
55/// Uses the native syntax of the machine the Agent runs on, such as `/home/user/project`,
56/// `C:\Users\user\project`, or `\\server\share\project`. It is not a URI.
57#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, From)]
59#[serde(transparent)]
60#[from(forward)]
61#[non_exhaustive]
62pub struct AbsolutePath(pub PathBuf);
63
64impl AbsolutePath {
65    /// Wraps a filesystem path as a typed [`AbsolutePath`].
66    #[must_use]
67    pub fn new(path: impl Into<Self>) -> Self {
68        path.into()
69    }
70
71    /// Returns the wrapped filesystem path.
72    #[must_use]
73    pub fn into_inner(self) -> PathBuf {
74        self.0
75    }
76}
77
78impl AsRef<Path> for AbsolutePath {
79    fn as_ref(&self) -> &Path {
80        self.0.as_path()
81    }
82}
83
84impl AsRef<OsStr> for AbsolutePath {
85    fn as_ref(&self) -> &OsStr {
86        self.0.as_os_str()
87    }
88}
89
90macro_rules! impl_into_option_conversion {
91    ($target:ty, $source:ty) => {
92        impl crate::IntoOption<$target> for $source {
93            fn into_option(self) -> Option<$target> {
94                Some(self.into())
95            }
96        }
97    };
98}
99
100macro_rules! impl_into_maybe_undefined_conversion {
101    ($target:ty, $source:ty) => {
102        impl crate::IntoMaybeUndefined<$target> for $source {
103            fn into_maybe_undefined(self) -> crate::MaybeUndefined<$target> {
104                crate::MaybeUndefined::Value(self.into())
105            }
106        }
107    };
108}
109
110impl_into_option_conversion!(AbsolutePath, PathBuf);
111impl_into_option_conversion!(AbsolutePath, OsString);
112impl_into_option_conversion!(AbsolutePath, String);
113impl_into_option_conversion!(AbsolutePath, Box<Path>);
114impl_into_option_conversion!(AbsolutePath, Cow<'_, Path>);
115impl_into_maybe_undefined_conversion!(AbsolutePath, PathBuf);
116impl_into_maybe_undefined_conversion!(AbsolutePath, OsString);
117impl_into_maybe_undefined_conversion!(AbsolutePath, String);
118impl_into_maybe_undefined_conversion!(AbsolutePath, Box<Path>);
119impl_into_maybe_undefined_conversion!(AbsolutePath, Cow<'_, Path>);
120
121impl<T: ?Sized + AsRef<OsStr>> crate::IntoOption<AbsolutePath> for &T {
122    fn into_option(self) -> Option<AbsolutePath> {
123        Some(self.into())
124    }
125}
126
127impl<T: ?Sized + AsRef<OsStr>> crate::IntoMaybeUndefined<AbsolutePath> for &T {
128    fn into_maybe_undefined(self) -> crate::MaybeUndefined<AbsolutePath> {
129        crate::MaybeUndefined::Value(self.into())
130    }
131}
132/// A unique identifier for a conversation session between a client and agent.
133///
134/// Sessions maintain their own context, conversation history, and state,
135/// allowing multiple independent interactions with the same agent.
136///
137/// See protocol docs: [Session ID](https://agentclientprotocol.com/protocol/v2/session-setup#session-id)
138#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
139#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
140#[serde(transparent)]
141#[from(forward)]
142#[non_exhaustive]
143pub struct SessionId(pub Arc<str>);
144
145impl SessionId {
146    /// Wraps a protocol string as a typed [`SessionId`].
147    #[must_use]
148    pub fn new(id: impl Into<Self>) -> Self {
149        id.into()
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn semantic_newtype_builders_remain_ergonomic() {
159        let request = NewSessionRequest::new("/workspace")
160            .additional_directories(["/workspace/shared", "/workspace/docs"]);
161        assert_eq!(request.cwd, AbsolutePath::new("/workspace"));
162        assert_eq!(
163            <AbsolutePath as AsRef<Path>>::as_ref(&request.cwd),
164            Path::new("/workspace")
165        );
166        assert_eq!(
167            <AbsolutePath as AsRef<OsStr>>::as_ref(&request.cwd),
168            OsStr::new("/workspace")
169        );
170
171        let list = ListSessionsRequest::new()
172            .cwd("/workspace")
173            .cursor("next-page");
174        assert_eq!(list.cursor, Some(SessionListCursor::new("next-page")));
175        assert_eq!(
176            <SessionListCursor as AsRef<str>>::as_ref(list.cursor.as_ref().unwrap()),
177            "next-page"
178        );
179
180        let image = ImageContent::new("aGVsbG8=", "image/png");
181        assert_eq!(image.mime_type, MediaType::new("image/png"));
182        assert_eq!(
183            <MediaType as AsRef<str>>::as_ref(&image.mime_type),
184            "image/png"
185        );
186
187        let session_id_source = String::from("session-1");
188        let session_id = SessionId::new(session_id_source.as_str());
189        assert_eq!(SessionId::new(session_id).to_string(), "session-1");
190
191        let os_path = OsString::from("/workspace");
192        assert_eq!(
193            ListSessionsRequest::new().cwd(&os_path).cwd,
194            Some(AbsolutePath::new(&os_path))
195        );
196        drop(TerminalUpdate::new("terminal-1").cwd(&os_path));
197
198        drop(ListSessionsRequest::new().cwd(None).cursor(None));
199        drop(ListSessionsResponse::new(Vec::new()).next_cursor(None));
200        drop(Icon::new("https://example.com/icon.png").mime_type(None));
201        drop(TerminalUpdate::new("terminal-1").cwd(None));
202    }
203}