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