Skip to content
Draft
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
9 changes: 6 additions & 3 deletions rust/src/generated/api_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3477,7 +3477,6 @@ impl CatalogAiSkillCandidate {
serde::de::value::StringDeserializer::<D::Error>::new(value),
)
}

fn deserialize_kind<'de, D>(deserializer: D) -> Result<CatalogAiSkillCandidateKind, D::Error>
where
D: serde::Deserializer<'de>,
Expand All @@ -3490,7 +3489,6 @@ impl CatalogAiSkillCandidate {
serde::de::value::StringDeserializer::<D::Error>::new(value),
)
}

fn deserialize_media_type<'de, D>(deserializer: D) -> Result<CatalogAiSkillMediaType, D::Error>
where
D: serde::Deserializer<'de>,
Expand Down Expand Up @@ -9962,7 +9960,6 @@ impl McpPlanRequiredValueEnum {
serde::de::value::StringDeserializer::<D::Error>::new(value),
)
}

fn deserialize_value_type<'de, D>(deserializer: D) -> Result<McpPlanEnumValueType, D::Error>
where
D: serde::Deserializer<'de>,
Expand Down Expand Up @@ -16880,6 +16877,9 @@ pub struct SendMessageItem {
/// If set, the request will fail if the named tool is not available when this message is among the user messages at the start of the current exchange
#[serde(skip_serializing_if = "Option::is_none")]
pub required_tool: Option<String>,
/// If true, this automatic notification may complete without visible assistant output. Defaults to false. Required messages in the same turn still require output; provider and execution errors are unaffected.
#[serde(skip_serializing_if = "Option::is_none")]
pub response_optional: Option<bool>,
/// Optional provenance tag copied to the resulting user.message event. Must be `user`, `system`, `command-<command-id>` for command-originated messages, `schedule-<numeric-id>` for scheduled prompts, or `agent-<agent-id>` for prompts sent by another agent.
#[doc(hidden)]
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down Expand Up @@ -16998,6 +16998,9 @@ pub struct SendRequest {
/// Provider-native output format for this turn, including all tool-call iterations. Not inherited by later turns or subagents. Ordinary steering inherits the active format; specifying responseFormat with mode: immediate is an error, even while idle. Returned assistant content remains text; the runtime does not parse or validate it. Unsupported models or schemas produce provider errors.
#[serde(skip_serializing_if = "Option::is_none")]
pub response_format: Option<SendRequestResponseFormat>,
/// If true, this automatic notification may complete without visible assistant output. Defaults to false. Required messages in the same turn still require output; provider and execution errors are unaffected.
#[serde(skip_serializing_if = "Option::is_none")]
pub response_optional: Option<bool>,
/// Optional provenance tag copied to the resulting user.message event. Must be `user`, `system`, `command-<command-id>` for command-originated messages, `schedule-<numeric-id>` for scheduled prompts, or `agent-<agent-id>` for prompts sent by another agent.
#[doc(hidden)]
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down
3 changes: 3 additions & 0 deletions rust/src/generated/session_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2598,6 +2598,9 @@ pub struct UserMessageData {
/// Parent agent task ID for background telemetry correlated to this user turn
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_agent_task_id: Option<String>,
/// True when the sender explicitly allows this automatic notification to complete without visible assistant output. Absent means a response is required. Does not relax other messages in the same turn or suppress execution errors.
#[serde(skip_serializing_if = "Option::is_none")]
pub response_optional: Option<bool>,
/// Responses reasoning settings anchored before this model-facing message, for cache-stable history replay
#[serde(skip_serializing_if = "Option::is_none")]
pub responses_reasoning: Option<ResponsesReasoning>,
Expand Down
3 changes: 3 additions & 0 deletions rust/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,9 @@ impl Session {
if let Some(source) = opts.source {
params["source"] = serde_json::to_value(source)?;
}
if let Some(response_optional) = opts.response_optional {
params["responseOptional"] = serde_json::json!(response_optional);
}
if let Some(m) = opts.mode {
params["mode"] = serde_json::to_value(m)?;
}
Expand Down
12 changes: 12 additions & 0 deletions rust/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5538,6 +5538,10 @@ pub struct MessageOptions {
/// Optional message provenance. When `None`, the field is omitted,
/// preserving the runtime's default for user messages.
pub source: Option<MessageSource>,
/// Whether the runtime may complete without producing an assistant response.
/// Omitted or false preserves the empty-output error. Required messages in a
/// combined turn take precedence over optional messages.
pub response_optional: Option<bool>,
/// Optional message delivery mode for this turn.
///
/// Controls whether the prompt is queued behind in-flight work
Expand Down Expand Up @@ -5579,6 +5583,7 @@ impl MessageOptions {
prompt: prompt.into(),
response_schema: None,
source: None,
response_optional: None,
mode: None,
agent_mode: None,
attachments: None,
Expand All @@ -5602,6 +5607,13 @@ impl MessageOptions {
self
}

/// Allow an empty completion for an informational message when true.
/// False retains the runtime's normal response requirement.
pub fn with_response_optional(mut self, response_optional: bool) -> Self {
self.response_optional = Some(response_optional);
self
}

/// Set the message delivery mode for this turn.
///
/// Pass [`DeliveryMode::Immediate`] to interrupt the session and run
Expand Down
111 changes: 110 additions & 1 deletion rust/tests/session_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2080,6 +2080,112 @@ async fn send_injects_session_id() {
timeout(TIMEOUT, handle).await.unwrap().unwrap().unwrap();
}

#[test]
fn message_options_response_optional_defaults_and_builder() {
let prompt = "notification".to_string();
for options in [
MessageOptions::new(&prompt),
MessageOptions::from(prompt.as_str()),
MessageOptions::from(prompt.clone()),
MessageOptions::from(&prompt),
] {
assert_eq!(options.response_optional, None);
assert_eq!(
options
.with_response_optional(true)
.with_response_optional(false)
.response_optional,
Some(false)
);
}
}

#[tokio::test]
async fn send_response_optional_preserves_false_and_omits_none() {
let (session, mut server) = create_session_pair().await;
let session = Arc::new(session);
for value in [None, Some(false), Some(true)] {
let mut options = MessageOptions::new("notification")
.with_source(MessageSource::System)
.with_mode(DeliveryMode::Enqueue);
let mut expected = serde_json::json!({
"sessionId": server.session_id,
"prompt": "notification",
"source": "system",
"mode": "enqueue",
});
if let Some(value) = value {
options = options.with_response_optional(value);
expected["responseOptional"] = serde_json::json!(value);
}
let handle = tokio::spawn({
let session = session.clone();
async move { session.send(options).await }
});
let request = timeout(TIMEOUT, server.read_request()).await.unwrap();
assert_eq!(request["method"], "session.send");
assert_eq!(request["params"], expected);
server
.respond(&request, serde_json::json!({"messageId": "optional"}))
.await;
assert_eq!(
timeout(TIMEOUT, handle).await.unwrap().unwrap().unwrap(),
"optional"
);
}
}

#[test]
fn rpc_response_optional_serialization() {
for value in [None, Some(false), Some(true)] {
let mut options = SendRequest::default();
options.prompt = "notification".into();
options.response_optional = value;
let mut expected = serde_json::json!({"prompt": "notification"});
if let Some(value) = value {
expected["responseOptional"] = serde_json::json!(value);
}
assert_eq!(serde_json::to_value(options).unwrap(), expected);
}
}

#[tokio::test]
async fn rpc_send_response_optional_preserves_false_and_omits_none() {
let (session, mut server) = create_session_pair().await;
let session = Arc::new(session);
for value in [None, Some(false), Some(true)] {
let mut options = SendRequest::default();
options.prompt = "notification".into();
options.response_optional = value;
let mut expected = serde_json::json!({
"sessionId": server.session_id,
"prompt": "notification",
});
if let Some(value) = value {
expected["responseOptional"] = serde_json::json!(value);
}
let handle = tokio::spawn({
let session = session.clone();
async move { session.rpc().send(options).await }
});
let request = timeout(TIMEOUT, server.read_request()).await.unwrap();
assert_eq!(request["method"], "session.send");
assert_eq!(request["params"], expected);
server
.respond(&request, serde_json::json!({"messageId": "optional"}))
.await;
assert_eq!(
timeout(TIMEOUT, handle)
.await
.unwrap()
.unwrap()
.unwrap()
.message_id,
"optional"
);
}
}

#[test]
fn message_options_source_is_opt_in() {
let prompt = "hello".to_string();
Expand Down Expand Up @@ -4370,7 +4476,7 @@ async fn send_and_wait_agent_source_preserves_mode_and_optional_reply() {
}

#[tokio::test]
async fn send_and_wait_system_source_returns_none_on_idle_without_assistant() {
async fn send_and_wait_response_optional_returns_none_on_idle_without_assistant() {
let (session, mut server) = create_session_pair().await;
let session = Arc::new(session);

Expand All @@ -4381,6 +4487,7 @@ async fn send_and_wait_system_source_returns_none_on_idle_without_assistant() {
.send_and_wait(
MessageOptions::new("Context updated")
.with_source(MessageSource::System)
.with_response_optional(true)
.with_wait_timeout(TIMEOUT),
)
.await
Expand All @@ -4389,6 +4496,7 @@ async fn send_and_wait_system_source_returns_none_on_idle_without_assistant() {
let request = timeout(TIMEOUT, server.read_request()).await.unwrap();
assert_eq!(request["method"], "session.send");
assert_eq!(request["params"]["source"], "system");
assert_eq!(request["params"]["responseOptional"], true);
server
.respond(&request, serde_json::json!({"messageId": "system-message"}))
.await;
Expand All @@ -4407,6 +4515,7 @@ async fn send_and_wait_system_source_returns_none_on_idle_without_assistant() {
let handle = tokio::spawn(async move { session.send("human follow-up").await });
let request = timeout(TIMEOUT, server.read_request()).await.unwrap();
assert!(request["params"].get("source").is_none());
assert!(request["params"].get("responseOptional").is_none());
server
.respond(&request, serde_json::json!({"messageId": "human-message"}))
.await;
Expand Down
Loading