Skip to content

Commit 2e77fb6

Browse files
ellismgCopilot
andcommitted
Add optional response contract to Rust SDK messages
Expose response_optional and its builder on MessageOptions, forwarding explicit true and false while omitting None. Cover high-level and generated session.send serialization and idle-only completion. Regenerate Rust types from pinned CLI 1.0.87-0 schemas with only the authoritative responseOptional property additions from the rebased runtime producer (SendRequest, SendMessageItem, UserMessageData), avoiding unrelated unpublished schema drift. Preserve upstream structured response support. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent cb2a8cc commit 2e77fb6

5 files changed

Lines changed: 134 additions & 4 deletions

File tree

‎rust/src/generated/api_types.rs‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3477,7 +3477,6 @@ impl CatalogAiSkillCandidate {
34773477
serde::de::value::StringDeserializer::<D::Error>::new(value),
34783478
)
34793479
}
3480-
34813480
fn deserialize_kind<'de, D>(deserializer: D) -> Result<CatalogAiSkillCandidateKind, D::Error>
34823481
where
34833482
D: serde::Deserializer<'de>,
@@ -3490,7 +3489,6 @@ impl CatalogAiSkillCandidate {
34903489
serde::de::value::StringDeserializer::<D::Error>::new(value),
34913490
)
34923491
}
3493-
34943492
fn deserialize_media_type<'de, D>(deserializer: D) -> Result<CatalogAiSkillMediaType, D::Error>
34953493
where
34963494
D: serde::Deserializer<'de>,
@@ -9962,7 +9960,6 @@ impl McpPlanRequiredValueEnum {
99629960
serde::de::value::StringDeserializer::<D::Error>::new(value),
99639961
)
99649962
}
9965-
99669963
fn deserialize_value_type<'de, D>(deserializer: D) -> Result<McpPlanEnumValueType, D::Error>
99679964
where
99689965
D: serde::Deserializer<'de>,
@@ -16880,6 +16877,9 @@ pub struct SendMessageItem {
1688016877
/// 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
1688116878
#[serde(skip_serializing_if = "Option::is_none")]
1688216879
pub required_tool: Option<String>,
16880+
/// 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.
16881+
#[serde(skip_serializing_if = "Option::is_none")]
16882+
pub response_optional: Option<bool>,
1688316883
/// 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.
1688416884
#[doc(hidden)]
1688516885
#[serde(skip_serializing_if = "Option::is_none")]
@@ -16998,6 +16998,9 @@ pub struct SendRequest {
1699816998
/// 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.
1699916999
#[serde(skip_serializing_if = "Option::is_none")]
1700017000
pub response_format: Option<SendRequestResponseFormat>,
17001+
/// 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.
17002+
#[serde(skip_serializing_if = "Option::is_none")]
17003+
pub response_optional: Option<bool>,
1700117004
/// 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.
1700217005
#[doc(hidden)]
1700317006
#[serde(skip_serializing_if = "Option::is_none")]

‎rust/src/generated/session_events.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2598,6 +2598,9 @@ pub struct UserMessageData {
25982598
/// Parent agent task ID for background telemetry correlated to this user turn
25992599
#[serde(skip_serializing_if = "Option::is_none")]
26002600
pub parent_agent_task_id: Option<String>,
2601+
/// 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.
2602+
#[serde(skip_serializing_if = "Option::is_none")]
2603+
pub response_optional: Option<bool>,
26012604
/// Responses reasoning settings anchored before this model-facing message, for cache-stable history replay
26022605
#[serde(skip_serializing_if = "Option::is_none")]
26032606
pub responses_reasoning: Option<ResponsesReasoning>,

‎rust/src/session.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,9 @@ impl Session {
609609
if let Some(source) = opts.source {
610610
params["source"] = serde_json::to_value(source)?;
611611
}
612+
if let Some(response_optional) = opts.response_optional {
613+
params["responseOptional"] = serde_json::json!(response_optional);
614+
}
612615
if let Some(m) = opts.mode {
613616
params["mode"] = serde_json::to_value(m)?;
614617
}

‎rust/src/types.rs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5538,6 +5538,10 @@ pub struct MessageOptions {
55385538
/// Optional message provenance. When `None`, the field is omitted,
55395539
/// preserving the runtime's default for user messages.
55405540
pub source: Option<MessageSource>,
5541+
/// Whether the runtime may complete without producing an assistant response.
5542+
/// Omitted or false preserves the empty-output error. Required messages in a
5543+
/// combined turn take precedence over optional messages.
5544+
pub response_optional: Option<bool>,
55415545
/// Optional message delivery mode for this turn.
55425546
///
55435547
/// Controls whether the prompt is queued behind in-flight work
@@ -5579,6 +5583,7 @@ impl MessageOptions {
55795583
prompt: prompt.into(),
55805584
response_schema: None,
55815585
source: None,
5586+
response_optional: None,
55825587
mode: None,
55835588
agent_mode: None,
55845589
attachments: None,
@@ -5602,6 +5607,13 @@ impl MessageOptions {
56025607
self
56035608
}
56045609

5610+
/// Allow an empty completion for an informational message when true.
5611+
/// False retains the runtime's normal response requirement.
5612+
pub fn with_response_optional(mut self, response_optional: bool) -> Self {
5613+
self.response_optional = Some(response_optional);
5614+
self
5615+
}
5616+
56055617
/// Set the message delivery mode for this turn.
56065618
///
56075619
/// Pass [`DeliveryMode::Immediate`] to interrupt the session and run

‎rust/tests/session_test.rs‎

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2080,6 +2080,112 @@ async fn send_injects_session_id() {
20802080
timeout(TIMEOUT, handle).await.unwrap().unwrap().unwrap();
20812081
}
20822082

2083+
#[test]
2084+
fn message_options_response_optional_defaults_and_builder() {
2085+
let prompt = "notification".to_string();
2086+
for options in [
2087+
MessageOptions::new(&prompt),
2088+
MessageOptions::from(prompt.as_str()),
2089+
MessageOptions::from(prompt.clone()),
2090+
MessageOptions::from(&prompt),
2091+
] {
2092+
assert_eq!(options.response_optional, None);
2093+
assert_eq!(
2094+
options
2095+
.with_response_optional(true)
2096+
.with_response_optional(false)
2097+
.response_optional,
2098+
Some(false)
2099+
);
2100+
}
2101+
}
2102+
2103+
#[tokio::test]
2104+
async fn send_response_optional_preserves_false_and_omits_none() {
2105+
let (session, mut server) = create_session_pair().await;
2106+
let session = Arc::new(session);
2107+
for value in [None, Some(false), Some(true)] {
2108+
let mut options = MessageOptions::new("notification")
2109+
.with_source(MessageSource::System)
2110+
.with_mode(DeliveryMode::Enqueue);
2111+
let mut expected = serde_json::json!({
2112+
"sessionId": server.session_id,
2113+
"prompt": "notification",
2114+
"source": "system",
2115+
"mode": "enqueue",
2116+
});
2117+
if let Some(value) = value {
2118+
options = options.with_response_optional(value);
2119+
expected["responseOptional"] = serde_json::json!(value);
2120+
}
2121+
let handle = tokio::spawn({
2122+
let session = session.clone();
2123+
async move { session.send(options).await }
2124+
});
2125+
let request = timeout(TIMEOUT, server.read_request()).await.unwrap();
2126+
assert_eq!(request["method"], "session.send");
2127+
assert_eq!(request["params"], expected);
2128+
server
2129+
.respond(&request, serde_json::json!({"messageId": "optional"}))
2130+
.await;
2131+
assert_eq!(
2132+
timeout(TIMEOUT, handle).await.unwrap().unwrap().unwrap(),
2133+
"optional"
2134+
);
2135+
}
2136+
}
2137+
2138+
#[test]
2139+
fn rpc_response_optional_serialization() {
2140+
for value in [None, Some(false), Some(true)] {
2141+
let mut options = SendRequest::default();
2142+
options.prompt = "notification".into();
2143+
options.response_optional = value;
2144+
let mut expected = serde_json::json!({"prompt": "notification"});
2145+
if let Some(value) = value {
2146+
expected["responseOptional"] = serde_json::json!(value);
2147+
}
2148+
assert_eq!(serde_json::to_value(options).unwrap(), expected);
2149+
}
2150+
}
2151+
2152+
#[tokio::test]
2153+
async fn rpc_send_response_optional_preserves_false_and_omits_none() {
2154+
let (session, mut server) = create_session_pair().await;
2155+
let session = Arc::new(session);
2156+
for value in [None, Some(false), Some(true)] {
2157+
let mut options = SendRequest::default();
2158+
options.prompt = "notification".into();
2159+
options.response_optional = value;
2160+
let mut expected = serde_json::json!({
2161+
"sessionId": server.session_id,
2162+
"prompt": "notification",
2163+
});
2164+
if let Some(value) = value {
2165+
expected["responseOptional"] = serde_json::json!(value);
2166+
}
2167+
let handle = tokio::spawn({
2168+
let session = session.clone();
2169+
async move { session.rpc().send(options).await }
2170+
});
2171+
let request = timeout(TIMEOUT, server.read_request()).await.unwrap();
2172+
assert_eq!(request["method"], "session.send");
2173+
assert_eq!(request["params"], expected);
2174+
server
2175+
.respond(&request, serde_json::json!({"messageId": "optional"}))
2176+
.await;
2177+
assert_eq!(
2178+
timeout(TIMEOUT, handle)
2179+
.await
2180+
.unwrap()
2181+
.unwrap()
2182+
.unwrap()
2183+
.message_id,
2184+
"optional"
2185+
);
2186+
}
2187+
}
2188+
20832189
#[test]
20842190
fn message_options_source_is_opt_in() {
20852191
let prompt = "hello".to_string();
@@ -4370,7 +4476,7 @@ async fn send_and_wait_agent_source_preserves_mode_and_optional_reply() {
43704476
}
43714477

43724478
#[tokio::test]
4373-
async fn send_and_wait_system_source_returns_none_on_idle_without_assistant() {
4479+
async fn send_and_wait_response_optional_returns_none_on_idle_without_assistant() {
43744480
let (session, mut server) = create_session_pair().await;
43754481
let session = Arc::new(session);
43764482

@@ -4381,6 +4487,7 @@ async fn send_and_wait_system_source_returns_none_on_idle_without_assistant() {
43814487
.send_and_wait(
43824488
MessageOptions::new("Context updated")
43834489
.with_source(MessageSource::System)
4490+
.with_response_optional(true)
43844491
.with_wait_timeout(TIMEOUT),
43854492
)
43864493
.await
@@ -4389,6 +4496,7 @@ async fn send_and_wait_system_source_returns_none_on_idle_without_assistant() {
43894496
let request = timeout(TIMEOUT, server.read_request()).await.unwrap();
43904497
assert_eq!(request["method"], "session.send");
43914498
assert_eq!(request["params"]["source"], "system");
4499+
assert_eq!(request["params"]["responseOptional"], true);
43924500
server
43934501
.respond(&request, serde_json::json!({"messageId": "system-message"}))
43944502
.await;
@@ -4407,6 +4515,7 @@ async fn send_and_wait_system_source_returns_none_on_idle_without_assistant() {
44074515
let handle = tokio::spawn(async move { session.send("human follow-up").await });
44084516
let request = timeout(TIMEOUT, server.read_request()).await.unwrap();
44094517
assert!(request["params"].get("source").is_none());
4518+
assert!(request["params"].get("responseOptional").is_none());
44104519
server
44114520
.respond(&request, serde_json::json!({"messageId": "human-message"}))
44124521
.await;

0 commit comments

Comments
 (0)