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::{JsonRpcBatch, 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#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, From)]
56#[serde(transparent)]
57#[from(forward)]
58#[non_exhaustive]
59pub struct AbsolutePath(pub PathBuf);
60
61impl AbsolutePath {
62    /// Wraps a filesystem path as a typed [`AbsolutePath`].
63    #[must_use]
64    pub fn new(path: impl Into<Self>) -> Self {
65        path.into()
66    }
67
68    /// Returns the wrapped filesystem path.
69    #[must_use]
70    pub fn into_inner(self) -> PathBuf {
71        self.0
72    }
73}
74
75impl AsRef<Path> for AbsolutePath {
76    fn as_ref(&self) -> &Path {
77        self.0.as_path()
78    }
79}
80
81impl AsRef<OsStr> for AbsolutePath {
82    fn as_ref(&self) -> &OsStr {
83        self.0.as_os_str()
84    }
85}
86
87macro_rules! impl_into_option_conversion {
88    ($target:ty, $source:ty) => {
89        impl crate::IntoOption<$target> for $source {
90            fn into_option(self) -> Option<$target> {
91                Some(self.into())
92            }
93        }
94    };
95}
96
97macro_rules! impl_into_maybe_undefined_conversion {
98    ($target:ty, $source:ty) => {
99        impl crate::IntoMaybeUndefined<$target> for $source {
100            fn into_maybe_undefined(self) -> crate::MaybeUndefined<$target> {
101                crate::MaybeUndefined::Value(self.into())
102            }
103        }
104    };
105}
106
107impl_into_option_conversion!(AbsolutePath, PathBuf);
108impl_into_option_conversion!(AbsolutePath, OsString);
109impl_into_option_conversion!(AbsolutePath, String);
110impl_into_option_conversion!(AbsolutePath, Box<Path>);
111impl_into_option_conversion!(AbsolutePath, Cow<'_, Path>);
112impl_into_maybe_undefined_conversion!(AbsolutePath, PathBuf);
113impl_into_maybe_undefined_conversion!(AbsolutePath, OsString);
114impl_into_maybe_undefined_conversion!(AbsolutePath, String);
115impl_into_maybe_undefined_conversion!(AbsolutePath, Box<Path>);
116impl_into_maybe_undefined_conversion!(AbsolutePath, Cow<'_, Path>);
117
118impl<T: ?Sized + AsRef<OsStr>> crate::IntoOption<AbsolutePath> for &T {
119    fn into_option(self) -> Option<AbsolutePath> {
120        Some(self.into())
121    }
122}
123
124impl<T: ?Sized + AsRef<OsStr>> crate::IntoMaybeUndefined<AbsolutePath> for &T {
125    fn into_maybe_undefined(self) -> crate::MaybeUndefined<AbsolutePath> {
126        crate::MaybeUndefined::Value(self.into())
127    }
128}
129/// A unique identifier for a conversation session between a client and agent.
130///
131/// Sessions maintain their own context, conversation history, and state,
132/// allowing multiple independent interactions with the same agent.
133///
134/// See protocol docs: [Session ID](https://agentclientprotocol.com/protocol/v2/session-setup#session-id)
135#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
136#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Display, From)]
137#[serde(transparent)]
138#[from(forward)]
139#[non_exhaustive]
140pub struct SessionId(pub Arc<str>);
141
142impl SessionId {
143    /// Wraps a protocol string as a typed [`SessionId`].
144    #[must_use]
145    pub fn new(id: impl Into<Self>) -> Self {
146        id.into()
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn semantic_newtype_builders_remain_ergonomic() {
156        let request = NewSessionRequest::new("/workspace")
157            .additional_directories(["/workspace/shared", "/workspace/docs"]);
158        assert_eq!(request.cwd, AbsolutePath::new("/workspace"));
159        assert_eq!(
160            <AbsolutePath as AsRef<Path>>::as_ref(&request.cwd),
161            Path::new("/workspace")
162        );
163        assert_eq!(
164            <AbsolutePath as AsRef<OsStr>>::as_ref(&request.cwd),
165            OsStr::new("/workspace")
166        );
167
168        let list = ListSessionsRequest::new()
169            .cwd("/workspace")
170            .cursor("next-page");
171        assert_eq!(list.cursor, Some(SessionListCursor::new("next-page")));
172        assert_eq!(
173            <SessionListCursor as AsRef<str>>::as_ref(list.cursor.as_ref().unwrap()),
174            "next-page"
175        );
176
177        let image = ImageContent::new("aGVsbG8=", "image/png");
178        assert_eq!(image.mime_type, MediaType::new("image/png"));
179        assert_eq!(
180            <MediaType as AsRef<str>>::as_ref(&image.mime_type),
181            "image/png"
182        );
183
184        let session_id_source = String::from("session-1");
185        let session_id = SessionId::new(session_id_source.as_str());
186        assert_eq!(SessionId::new(session_id).to_string(), "session-1");
187
188        let os_path = OsString::from("/workspace");
189        assert_eq!(
190            ListSessionsRequest::new().cwd(&os_path).cwd,
191            Some(AbsolutePath::new(&os_path))
192        );
193        drop(TerminalUpdate::new("terminal-1").cwd(&os_path));
194
195        drop(ListSessionsRequest::new().cwd(None).cursor(None));
196        drop(ListSessionsResponse::new(Vec::new()).next_cursor(None));
197        drop(Icon::new("https://example.com/icon.png").mime_type(None));
198        drop(TerminalUpdate::new("terminal-1").cwd(None));
199    }
200}