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