Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 134 additions & 42 deletions agent-client-protocol-schema/src/v2/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1167,11 +1167,11 @@ pub struct NewSessionResponse {
///
/// Used in all subsequent requests for this conversation.
pub session_id: SessionId,
/// Initial session configuration options if supported by the Agent.
#[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
/// Initial session configuration options.
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
#[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
#[serde(default)]
pub config_options: Option<Vec<SessionConfigOption>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub config_options: Vec<SessionConfigOption>,
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
/// metadata to their interactions. Implementations MUST NOT make assumptions about values at
/// these keys.
Expand All @@ -1190,18 +1190,15 @@ impl NewSessionResponse {
pub fn new(session_id: impl Into<SessionId>) -> Self {
Self {
session_id: session_id.into(),
config_options: None,
config_options: Vec::new(),
meta: None,
}
}

/// Initial session configuration options if supported by the Agent.
/// Initial session configuration options.
#[must_use]
pub fn config_options(
mut self,
config_options: impl IntoOption<Vec<SessionConfigOption>>,
) -> Self {
self.config_options = config_options.into_option();
pub fn config_options(mut self, config_options: Vec<SessionConfigOption>) -> Self {
self.config_options = config_options;
self
}

Expand Down Expand Up @@ -1248,6 +1245,7 @@ pub struct LoadSessionRequest {
/// List of MCP servers to connect to for this session.
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
#[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub mcp_servers: Vec<McpServer>,
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
/// metadata to their interactions. Implementations MUST NOT make assumptions about values at
Expand Down Expand Up @@ -1308,11 +1306,11 @@ impl LoadSessionRequest {
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct LoadSessionResponse {
/// Initial session configuration options if supported by the Agent.
#[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
/// Initial session configuration options.
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
#[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
#[serde(default)]
pub config_options: Option<Vec<SessionConfigOption>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub config_options: Vec<SessionConfigOption>,
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
/// metadata to their interactions. Implementations MUST NOT make assumptions about values at
/// these keys.
Expand All @@ -1332,13 +1330,10 @@ impl LoadSessionResponse {
Self::default()
}

/// Initial session configuration options if supported by the Agent.
/// Initial session configuration options.
#[must_use]
pub fn config_options(
mut self,
config_options: impl IntoOption<Vec<SessionConfigOption>>,
) -> Self {
self.config_options = config_options.into_option();
pub fn config_options(mut self, config_options: Vec<SessionConfigOption>) -> Self {
self.config_options = config_options;
self
}

Expand Down Expand Up @@ -1459,11 +1454,11 @@ impl ForkSessionRequest {
pub struct ForkSessionResponse {
/// Unique identifier for the newly created forked session.
pub session_id: SessionId,
/// Initial session configuration options if supported by the Agent.
#[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
/// Initial session configuration options.
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
#[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
#[serde(default)]
pub config_options: Option<Vec<SessionConfigOption>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub config_options: Vec<SessionConfigOption>,
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
/// metadata to their interactions. Implementations MUST NOT make assumptions about values at
/// these keys.
Expand All @@ -1483,18 +1478,15 @@ impl ForkSessionResponse {
pub fn new(session_id: impl Into<SessionId>) -> Self {
Self {
session_id: session_id.into(),
config_options: None,
config_options: Vec::new(),
meta: None,
}
}

/// Initial session configuration options if supported by the Agent.
/// Initial session configuration options.
#[must_use]
pub fn config_options(
mut self,
config_options: impl IntoOption<Vec<SessionConfigOption>>,
) -> Self {
self.config_options = config_options.into_option();
pub fn config_options(mut self, config_options: Vec<SessionConfigOption>) -> Self {
self.config_options = config_options;
self
}

Expand Down Expand Up @@ -1603,11 +1595,11 @@ impl ResumeSessionRequest {
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ResumeSessionResponse {
/// Initial session configuration options if supported by the Agent.
#[serde_as(deserialize_as = "DefaultOnError<Option<VecSkipError<_, SkipListener>>>")]
/// Initial session configuration options.
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_, SkipListener>>")]
#[schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true))]
#[serde(default)]
pub config_options: Option<Vec<SessionConfigOption>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub config_options: Vec<SessionConfigOption>,
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
/// metadata to their interactions. Implementations MUST NOT make assumptions about values at
/// these keys.
Expand All @@ -1627,13 +1619,10 @@ impl ResumeSessionResponse {
Self::default()
}

/// Initial session configuration options if supported by the Agent.
/// Initial session configuration options.
#[must_use]
pub fn config_options(
mut self,
config_options: impl IntoOption<Vec<SessionConfigOption>>,
) -> Self {
self.config_options = config_options.into_option();
pub fn config_options(mut self, config_options: Vec<SessionConfigOption>) -> Self {
self.config_options = config_options;
self
}

Expand Down Expand Up @@ -5886,6 +5875,82 @@ mod test_serialization {
);
}

fn test_config_option() -> SessionConfigOption {
SessionConfigOption::select(
"mode",
"Mode",
"ask",
vec![SessionConfigSelectOption::new("ask", "Ask")],
)
}

#[test]
fn test_session_response_config_options_default_empty_and_skip_serializing() {
assert_eq!(
serde_json::to_value(NewSessionResponse::new("sess")).unwrap(),
json!({ "sessionId": "sess" })
);
assert_eq!(
serde_json::to_value(LoadSessionResponse::new()).unwrap(),
json!({})
);
assert_eq!(
serde_json::to_value(ResumeSessionResponse::new()).unwrap(),
json!({})
);
#[cfg(feature = "unstable_session_fork")]
assert_eq!(
serde_json::to_value(ForkSessionResponse::new("fork")).unwrap(),
json!({ "sessionId": "fork" })
);

let json = serde_json::to_value(
NewSessionResponse::new("sess").config_options(vec![test_config_option()]),
)
.unwrap();
assert_eq!(json["configOptions"].as_array().unwrap().len(), 1);
}

#[test]
fn test_session_response_config_options_deserialize_missing_null_and_invalid() {
let missing: NewSessionResponse =
serde_json::from_value(json!({ "sessionId": "sess" })).unwrap();
assert!(missing.config_options.is_empty());

let null: NewSessionResponse = serde_json::from_value(json!({
"sessionId": "sess",
"configOptions": null
}))
.unwrap();
assert!(null.config_options.is_empty());

let wrong_shape: NewSessionResponse = serde_json::from_value(json!({
"sessionId": "sess",
"configOptions": "oops"
}))
.unwrap();
assert!(wrong_shape.config_options.is_empty());

let valid_option = serde_json::to_value(test_config_option()).unwrap();
let mixed: NewSessionResponse = serde_json::from_value(json!({
"sessionId": "sess",
"configOptions": ["oops", valid_option]
}))
.unwrap();
assert_eq!(mixed.config_options.len(), 1);

let load: LoadSessionResponse = serde_json::from_value(json!({})).unwrap();
assert!(load.config_options.is_empty());
let resume: ResumeSessionResponse = serde_json::from_value(json!({})).unwrap();
assert!(resume.config_options.is_empty());
#[cfg(feature = "unstable_session_fork")]
{
let fork: ForkSessionResponse =
serde_json::from_value(json!({ "sessionId": "fork" })).unwrap();
assert!(fork.config_options.is_empty());
}
}

#[test]
fn test_auth_method_agent_serialization() {
let method = AuthMethod::Agent(AuthMethodAgent::new("default-auth", "Default Auth"));
Expand Down Expand Up @@ -6054,6 +6119,33 @@ mod test_serialization {
],
})
);
assert_eq!(
serde_json::to_value(LoadSessionRequest::new("sess_abc123", "/home/user/project"))
.unwrap(),
json!({
"sessionId": "sess_abc123",
"cwd": "/home/user/project",
})
);
assert_eq!(
serde_json::from_value::<LoadSessionRequest>(json!({
"sessionId": "sess_abc123",
"cwd": "/home/user/project"
}))
.unwrap()
.mcp_servers,
Vec::<McpServer>::new()
);
assert_eq!(
serde_json::from_value::<LoadSessionRequest>(json!({
"sessionId": "sess_abc123",
"cwd": "/home/user/project",
"mcpServers": null
}))
.unwrap()
.mcp_servers,
Vec::<McpServer>::new()
);
assert_eq!(
serde_json::to_value(SessionInfo::new("sess_abc123", "/home/user/project")).unwrap(),
json!({
Expand Down
49 changes: 41 additions & 8 deletions agent-client-protocol-schema/src/v2/conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,13 @@ where
value.map(into_v2_vec_skip_errors)
}

fn option_vec_into_v2_default_skip_errors<T>(value: Option<Vec<T>>) -> Vec<T::Output>
where
T: IntoV2,
{
value.map(into_v2_vec_skip_errors).unwrap_or_default()
}

impl<T> IntoV2 for Vec<T>
where
T: IntoV2,
Expand Down Expand Up @@ -3233,7 +3240,7 @@ impl IntoV1 for super::NewSessionResponse {
Ok(crate::v1::NewSessionResponse {
session_id: session_id.into_v1()?,
modes: None,
config_options: option_vec_into_v1_skip_errors(config_options),
config_options: Some(into_v1_vec_skip_errors(config_options)),
meta: meta.into_v1()?,
})
}
Expand All @@ -3251,7 +3258,7 @@ impl IntoV2 for crate::v1::NewSessionResponse {
} = self;
Ok(super::NewSessionResponse {
session_id: session_id.into_v2()?,
config_options: option_vec_into_v2_skip_errors(config_options),
config_options: option_vec_into_v2_default_skip_errors(config_options),
meta: meta.into_v2()?,
})
}
Expand Down Expand Up @@ -3309,7 +3316,7 @@ impl IntoV1 for super::LoadSessionResponse {
} = self;
Ok(crate::v1::LoadSessionResponse {
modes: None,
config_options: option_vec_into_v1_skip_errors(config_options),
config_options: Some(into_v1_vec_skip_errors(config_options)),
meta: meta.into_v1()?,
})
}
Expand All @@ -3325,7 +3332,7 @@ impl IntoV2 for crate::v1::LoadSessionResponse {
meta,
} = self;
Ok(super::LoadSessionResponse {
config_options: option_vec_into_v2_skip_errors(config_options),
config_options: option_vec_into_v2_default_skip_errors(config_options),
meta: meta.into_v2()?,
})
}
Expand Down Expand Up @@ -3388,7 +3395,7 @@ impl IntoV1 for super::ForkSessionResponse {
Ok(crate::v1::ForkSessionResponse {
session_id: session_id.into_v1()?,
modes: None,
config_options: option_vec_into_v1_skip_errors(config_options),
config_options: Some(into_v1_vec_skip_errors(config_options)),
meta: meta.into_v1()?,
})
}
Expand All @@ -3407,7 +3414,7 @@ impl IntoV2 for crate::v1::ForkSessionResponse {
} = self;
Ok(super::ForkSessionResponse {
session_id: session_id.into_v2()?,
config_options: option_vec_into_v2_skip_errors(config_options),
config_options: option_vec_into_v2_default_skip_errors(config_options),
meta: meta.into_v2()?,
})
}
Expand Down Expand Up @@ -3465,7 +3472,7 @@ impl IntoV1 for super::ResumeSessionResponse {
} = self;
Ok(crate::v1::ResumeSessionResponse {
modes: None,
config_options: option_vec_into_v1_skip_errors(config_options),
config_options: Some(into_v1_vec_skip_errors(config_options)),
meta: meta.into_v1()?,
})
}
Expand All @@ -3481,7 +3488,7 @@ impl IntoV2 for crate::v1::ResumeSessionResponse {
meta,
} = self;
Ok(super::ResumeSessionResponse {
config_options: option_vec_into_v2_skip_errors(config_options),
config_options: option_vec_into_v2_default_skip_errors(config_options),
meta: meta.into_v2()?,
})
}
Expand Down Expand Up @@ -9999,12 +10006,38 @@ mod tests {
assert!(back_to_v1.modes.is_none());
}

#[test]
fn v1_session_response_missing_config_options_becomes_empty_v2_vec() {
let new_response: v2::NewSessionResponse =
v1_to_v2(v1::NewSessionResponse::new("sess")).unwrap();
assert!(new_response.config_options.is_empty());

let load_response: v2::LoadSessionResponse =
v1_to_v2(v1::LoadSessionResponse::new()).unwrap();
assert!(load_response.config_options.is_empty());

let resume_response: v2::ResumeSessionResponse =
v1_to_v2(v1::ResumeSessionResponse::new()).unwrap();
assert!(resume_response.config_options.is_empty());

#[cfg(feature = "unstable_session_fork")]
{
let fork_response: v2::ForkSessionResponse =
v1_to_v2(v1::ForkSessionResponse::new("fork")).unwrap();
assert!(fork_response.config_options.is_empty());
}
}

#[test]
fn v2_session_response_converts_to_v1_without_mode_state() {
let response: v1::NewSessionResponse =
v2_to_v1(v2::NewSessionResponse::new("sess")).unwrap();

assert!(response.modes.is_none());
assert!(matches!(
response.config_options,
Some(config_options) if config_options.is_empty()
));
}

#[test]
Expand Down
Loading
Loading