diff --git a/rust/README.md b/rust/README.md index 254a2b216..054eb2804 100644 --- a/rust/README.md +++ b/rust/README.md @@ -89,7 +89,7 @@ With the default `CliProgram::Resolve`, `Client::start()` resolves the CLI in th Created via `Client::create_session` or `Client::resume_session`. Owns an internal event loop that dispatches CLI callbacks to the focused handler traits you install on `SessionConfig`, and broadcasts session events through `subscribe()`. ```rust,ignore -use github_copilot_sdk::MessageOptions; +use github_copilot_sdk::{InterruptMainTurnOptions, MessageOptions}; // Simple send — &str / String convert into MessageOptions automatically. // Returns the assigned message ID for correlation with later events. @@ -107,7 +107,14 @@ let _id = session // Message history let messages = session.get_events().await?; -// Abort the current agent turn +// Interrupt only the foreground turn and preserve queued user prompts. +let result = session + .interrupt_main_turn( + InterruptMainTurnOptions::default().with_flush_queued(true), + ) + .await?; + +// Recursively abort the active turn and all descendant/background work. session.abort().await?; // Model management @@ -176,6 +183,10 @@ let forked = client .await?; ``` +`interrupt_main_turn` preserves background agents and other independent background work. Its default `flush_queued: false` discards queued prompts; `true` preserves queued user prompts for the next eligible turn, drops hidden system prompts, and resumes normal queue delivery after the interrupted coordinator loop unwinds. A result with `interrupted == false` means the method is supported but no main turn was active. The method never falls back to `session.abort`, whose recursive cancellation also stops descendant and background work. + +Check `session.capabilities().interrupt_main_turn` before calling when connected targets may differ: local sessions report `Some(true)`, unsupported remote sessions report `Some(false)`, and older servers omit the capability (`None`). Each `capabilities.changed` event is a complete snapshot, so the SDK replaces the cached capability state wholesale rather than merging omitted fields. + New RPCs land in the namespace immediately as the schema regenerates; helpers are added on top only when an ergonomic story is worth the maintenance. diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index e243ec1dc..982184939 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -177,6 +177,8 @@ pub mod rpc_methods { pub const SESSION_SENDMESSAGES: &str = "session.sendMessages"; /// `session.abort` pub const SESSION_ABORT: &str = "session.abort"; + /// `session.interruptMainTurn` + pub const SESSION_INTERRUPTMAINTURN: &str = "session.interruptMainTurn"; /// `session.shutdown` pub const SESSION_SHUTDOWN: &str = "session.shutdown"; /// `session.gitHubAuth.getStatus` @@ -587,7 +589,7 @@ pub mod rpc_methods { pub const CANVAS_ACTION_INVOKE: &str = "canvas.action.invoke"; } -/// Parameters for aborting the current turn +/// Parameters for aborting the active session turn and recursively cancelling its descendant and background work /// ///
/// @@ -603,7 +605,7 @@ pub struct AbortRequest { pub reason: Option, } -/// Result of aborting the current turn +/// Result of recursively aborting the active session work /// ///
/// @@ -4314,6 +4316,37 @@ pub struct InstructionsGetSourcesResult { pub sources: Vec, } +/// Parameters for interrupting only the active main coordinator turn while preserving background work +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InterruptMainTurnRequest { + /// When an active main turn is interrupted, true flushes queued user prompts through to the next eligible turn: it preserves them, drops hidden system prompts, and resumes normal queue delivery after the interrupted loop unwinds (including existing deferred-idle ordering). False or omitted discards all queued prompts. When no main turn is active, this option has no effect. + #[serde(skip_serializing_if = "Option::is_none")] + pub flush_queued: Option, +} + +/// Result of interrupting only the active main coordinator turn +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InterruptMainTurnResult { + /// True when an active main coordinator turn was interrupted; false only when the method is supported but no main turn was active + pub interrupted: bool, +} + /// A request body chunk or cancellation signal. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -15100,6 +15133,9 @@ pub struct UsageMetricsModelMetricUsage { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct UsageMetricsModelMetric { + /// Latest known prompt-cache expiration for this model. A timestamp in the past indicates that the observed cache has expired. + #[serde(skip_serializing_if = "Option::is_none")] + pub cache_expires_at: Option, /// Request count and cost metrics for this model pub requests: UsageMetricsModelMetricRequests, /// Token count details per type @@ -16278,7 +16314,7 @@ pub struct SessionSendMessagesResult { pub message_ids: Vec, } -/// Result of aborting the current turn +/// Result of recursively aborting the active session work /// ///
/// @@ -16296,6 +16332,21 @@ pub struct SessionAbortResult { pub success: bool, } +/// Result of interrupting only the active main coordinator turn +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionInterruptMainTurnResult { + /// True when an active main coordinator turn was interrupted; false only when the method is supported but no main turn was active + pub interrupted: bool, +} + /// Identifies the target session. /// ///
@@ -20866,6 +20917,9 @@ pub enum HookType { /// Runs after the user submits a prompt. #[serde(rename = "userPromptSubmitted")] UserPromptSubmitted, + /// Runs after the runtime transforms the submitted prompt for the model, before it is added to session history. + #[serde(rename = "userPromptTransformed")] + UserPromptTransformed, /// Runs when a session starts. #[serde(rename = "sessionStart")] SessionStart, diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs index 403933a27..333cd5d84 100644 --- a/rust/src/generated/rpc.rs +++ b/rust/src/generated/rpc.rs @@ -2920,17 +2920,17 @@ impl<'a> SessionRpc<'a> { Ok(serde_json::from_value(_value)?) } - /// Aborts the current agent turn. + /// Aborts the active session turn and recursively cancels its descendant and background work. Use session.interruptMainTurn when background work must survive. /// /// Wire method: `session.abort`. /// /// # Parameters /// - /// * `params` - Parameters for aborting the current turn + /// * `params` - Parameters for aborting the active session turn and recursively cancelling its descendant and background work /// /// # Returns /// - /// Result of aborting the current turn + /// Result of recursively aborting the active session work /// ///
/// @@ -2950,6 +2950,39 @@ impl<'a> SessionRpc<'a> { Ok(serde_json::from_value(_value)?) } + /// Interrupts only the active main coordinator turn while preserving background agents and other background work. Returns interrupted=false only when the method is supported but no main turn is active. Unsupported or older targets return JSON-RPC MethodNotFound; callers must not fall back to session.abort unless recursive cancellation is intended. + /// + /// Wire method: `session.interruptMainTurn`. + /// + /// # Parameters + /// + /// * `params` - Parameters for interrupting only the active main coordinator turn while preserving background work + /// + /// # Returns + /// + /// Result of interrupting only the active main coordinator turn + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn interrupt_main_turn( + &self, + params: InterruptMainTurnRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_INTERRUPTMAINTURN, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Shuts down the session and persists its final state. Awaits any deferred sessionEnd hooks before resolving so user-supplied hook scripts complete before the runtime tears down. /// /// Wire method: `session.shutdown`. diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index cf670c485..5b75742bd 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -1209,10 +1209,27 @@ pub struct SessionShutdownData { pub(crate) total_premium_requests: Option, } +/// Internal prompt-cache expiration state for one model +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct UsageCheckpointModelCacheState { + /// Latest known prompt-cache expiration + pub cache_expires_at: String, + /// Retained cache lifetime in seconds, used to refresh expiration after a cache read + #[doc(hidden)] + pub(crate) cache_ttl_seconds: i64, + /// Model identifier associated with this cache state + pub model_id: String, +} + /// Session event "session.usage_checkpoint". Durable session usage checkpoint for reconstructing aggregate accounting on resume #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionUsageCheckpointData { + /// Internal per-model prompt-cache state used to restore expiration tracking on resume + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) model_cache_state: Option>, /// Session-wide accumulated nano-AI units cost at checkpoint time pub total_nano_aiu: f64, /// Total number of premium API requests used at checkpoint time @@ -1870,6 +1887,9 @@ pub struct AssistantUsageData { /// API endpoint used for this model call, matching CAPI supported_endpoints vocabulary #[serde(skip_serializing_if = "Option::is_none")] pub api_endpoint: Option, + /// Updated prompt-cache expiration for this model call. Present only when the call establishes or refreshes known cache state. + #[serde(skip_serializing_if = "Option::is_none")] + pub cache_expires_at: Option, /// Number of tokens read from prompt cache #[serde(skip_serializing_if = "Option::is_none")] pub cache_read_tokens: Option, @@ -4042,6 +4062,9 @@ pub struct CapabilitiesChangedUI { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CapabilitiesChangedData { + /// Whether scoped main-turn interruption via `session.interruptMainTurn` is supported + #[serde(skip_serializing_if = "Option::is_none")] + pub interrupt_main_turn: Option, /// UI capability changes #[serde(skip_serializing_if = "Option::is_none")] pub ui: Option, diff --git a/rust/src/session.rs b/rust/src/session.rs index 35656c657..6af97d09c 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -12,8 +12,8 @@ use tracing::{Instrument, warn}; use crate::canvas::CanvasHandler; use crate::generated::api_types::{ - LogRequest, ModelSwitchToRequest, OpenCanvasInstance, RegisterEventInterestParams, - ToolsGetCurrentMetadataResult, rpc_methods, + InterruptMainTurnRequest, InterruptMainTurnResult, LogRequest, ModelSwitchToRequest, + OpenCanvasInstance, RegisterEventInterestParams, ToolsGetCurrentMetadataResult, rpc_methods, }; use crate::generated::session_events::{ CommandExecuteData, ElicitationRequestedData, ExternalToolRequestedData, McpOauthRequiredData, @@ -31,9 +31,9 @@ use crate::trace_context::inject_trace_context; use crate::transforms::SystemMessageTransform; use crate::types::{ CommandContext, CommandDefinition, CommandHandler, CreateSessionResult, ElicitationRequest, - ElicitationResult, ExitPlanModeData, GetMessagesResponse, MessageOptions, - PermissionRequestData, RequestId, ResumeSessionConfig, ResumeSessionResult, SectionOverride, - SessionCapabilities, SessionConfig, SessionEvent, SessionId, SetModelOptions, + ElicitationResult, ExitPlanModeData, GetMessagesResponse, InterruptMainTurnOptions, + MessageOptions, PermissionRequestData, RequestId, ResumeSessionConfig, ResumeSessionResult, + SectionOverride, SessionCapabilities, SessionConfig, SessionEvent, SessionId, SetModelOptions, SystemMessageConfig, ToolInvocation, ToolResult, ToolResultExpanded, TraceContext, UiInputOptions, ensure_attachment_display_names, }; @@ -509,7 +509,11 @@ impl Session { self.get_events().await } - /// Abort the current agent turn. + /// Abort the active session turn and recursively cancel descendant and + /// background work. + /// + /// Use [`interrupt_main_turn`](Self::interrupt_main_turn) when independent + /// background work must survive. /// /// # Cancel safety /// @@ -526,6 +530,35 @@ impl Session { Ok(()) } + /// Interrupt only the active foreground/main turn while preserving + /// independent background agents and descendants. + /// + /// This never falls back to [`abort`](Self::abort), which is the recursive + /// destructive cancellation API. Check + /// [`SessionCapabilities::interrupt_main_turn`] before calling when the + /// client may connect to mixed CLI versions; unsupported runtimes return + /// JSON-RPC `MethodNotFound`. + /// + /// [`InterruptMainTurnOptions::default`] sends `flush_queued: false` and + /// discards queued prompts. Setting it to `true` preserves queued user + /// prompts for the next eligible turn, drops hidden system prompts, and + /// resumes normal delivery after the interrupted loop unwinds. + /// + /// # Cancel safety + /// + /// **Cancel-safe.** Single `session.interruptMainTurn` RPC; the underlying + /// [`Client::call`](crate::Client::call) is cancel-safe via the writer actor. + pub async fn interrupt_main_turn( + &self, + options: InterruptMainTurnOptions, + ) -> Result { + self.rpc() + .interrupt_main_turn(InterruptMainTurnRequest { + flush_queued: Some(options.flush_queued), + }) + .await + } + /// Switch to a different model. /// /// Pass `None` for `opts` if no extra configuration is needed. @@ -1610,7 +1643,9 @@ async fn handle_notification( // response to the event observe the new state. if event_type == SessionEventType::CapabilitiesChanged { match serde_json::from_value::(notification.event.data.clone()) { - Ok(changed) => *capabilities.write() = changed, + // Every capabilities.changed payload is a complete snapshot, so + // omitted fields intentionally clear values from the prior one. + Ok(snapshot) => *capabilities.write() = snapshot, Err(e) => warn!(error = %e, "failed to deserialize capabilities.changed payload"), } } diff --git a/rust/src/types.rs b/rust/src/types.rs index b34d8fff4..447757b3b 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -5235,12 +5235,38 @@ pub struct ElicitationRequest { /// Updated at runtime via `capabilities.changed` events. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] +#[non_exhaustive] pub struct SessionCapabilities { + /// Whether scoped foreground interruption via + /// [`Session::interrupt_main_turn`](crate::session::Session::interrupt_main_turn) + /// is supported by the connected CLI runtime. + /// + /// Local sessions report `Some(true)`, unsupported remote sessions report + /// `Some(false)`, and older servers omit the field (`None`). + #[serde(skip_serializing_if = "Option::is_none")] + pub interrupt_main_turn: Option, /// UI capabilities (elicitation support, etc.). #[serde(skip_serializing_if = "Option::is_none")] pub ui: Option, } +/// Options for [`Session::interrupt_main_turn`](crate::session::Session::interrupt_main_turn). +#[derive(Debug, Clone, Copy, Default)] +#[non_exhaustive] +pub struct InterruptMainTurnOptions { + /// Preserve queued user prompts for the next eligible turn. The default + /// (`false`) discards queued prompts while interrupting the foreground turn. + pub flush_queued: bool, +} + +impl InterruptMainTurnOptions { + /// Set whether queued user prompts should be preserved. + pub fn with_flush_queued(mut self, flush_queued: bool) -> Self { + self.flush_queued = flush_queued; + self + } +} + /// UI-specific capabilities for a session. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/rust/tests/e2e/elicitation.rs b/rust/tests/e2e/elicitation.rs index 5575e67f3..ac6f3bb8a 100644 --- a/rust/tests/e2e/elicitation.rs +++ b/rust/tests/e2e/elicitation.rs @@ -380,13 +380,12 @@ async fn elicitation_returns_all_action_shapes() { #[tokio::test] async fn session_capabilities_types_are_properly_structured() { - let capabilities = github_copilot_sdk::SessionCapabilities { - ui: Some(UiCapabilities { - elicitation: Some(true), - mcp_apps: None, - canvases: None, - }), - }; + let mut capabilities = github_copilot_sdk::SessionCapabilities::default(); + capabilities.ui = Some(UiCapabilities { + elicitation: Some(true), + mcp_apps: None, + canvases: None, + }); assert_eq!( capabilities.ui.as_ref().and_then(|ui| ui.elicitation), diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index 122ec475d..6849e6004 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -14,16 +14,16 @@ use github_copilot_sdk::handler::{ }; use github_copilot_sdk::rpc::{ CanvasProviderInvokeActionRequest, CanvasProviderOpenRequest, CanvasProviderOpenResult, - OpenCanvasInstance, + InterruptMainTurnRequest, OpenCanvasInstance, }; use github_copilot_sdk::session_events::{ - McpOauthRequiredData, ReasoningSummary, SessionLimitsConfig, + CapabilitiesChangedData, McpOauthRequiredData, ReasoningSummary, SessionLimitsConfig, }; use github_copilot_sdk::types::{ CanvasProviderIdentity, CloudSessionOptions, CloudSessionRepository, CommandContext, CommandDefinition, CommandHandler, DeliveryMode, ElicitationRequest, ElicitationResult, - ExitPlanModeData, ExtensionInfo, MessageOptions, RequestId, SessionConfig, SessionId, - SetModelOptions, Tool, ToolInvocation, ToolResult, + ExitPlanModeData, ExtensionInfo, InterruptMainTurnOptions, MessageOptions, RequestId, + SessionConfig, SessionId, SetModelOptions, Tool, ToolInvocation, ToolResult, }; use github_copilot_sdk::{Client, ContextTier, ErrorKind, ProtocolErrorKind, tool}; use serde_json::Value; @@ -133,6 +133,16 @@ impl FakeServer { write_framed(&mut self.write, &serde_json::to_vec(&response).unwrap()).await; } + async fn respond_error(&mut self, request: &Value, code: i32, message: &str) { + let id = request["id"].as_u64().unwrap(); + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": id, + "error": { "code": code, "message": message }, + }); + write_framed(&mut self.write, &serde_json::to_vec(&response).unwrap()).await; + } + async fn send_notification(&mut self, method: &str, params: Value) { let notification = serde_json::json!({ "jsonrpc": "2.0", @@ -1287,6 +1297,94 @@ async fn session_rpc_methods_send_correct_method_names() { } } +#[tokio::test] +async fn interrupt_main_turn_rpc_omits_default_flush_queued() { + let (session, mut server) = create_session_pair().await; + let handle = tokio::spawn(async move { + session + .rpc() + .interrupt_main_turn(InterruptMainTurnRequest::default()) + .await + }); + + let request = server.read_request().await; + assert_eq!(request["method"], "session.interruptMainTurn"); + assert_eq!( + request["params"], + serde_json::json!({ "sessionId": server.session_id }) + ); + server + .respond(&request, serde_json::json!({ "interrupted": true })) + .await; + + let result = timeout(TIMEOUT, handle).await.unwrap().unwrap().unwrap(); + assert!(result.interrupted); +} + +#[tokio::test] +async fn interrupt_main_turn_sends_false_and_true_and_returns_status() { + let (session, mut server) = create_session_pair().await; + let session = Arc::new(session); + + for (options, interrupted) in [ + (InterruptMainTurnOptions::default(), false), + ( + InterruptMainTurnOptions::default().with_flush_queued(true), + true, + ), + ] { + let handle = tokio::spawn({ + let session = session.clone(); + async move { session.interrupt_main_turn(options).await } + }); + + let request = server.read_request().await; + assert_eq!(request["method"], "session.interruptMainTurn"); + assert_eq!( + request["params"], + serde_json::json!({ + "sessionId": server.session_id, + "flushQueued": options.flush_queued, + }) + ); + server + .respond(&request, serde_json::json!({ "interrupted": interrupted })) + .await; + + let result = timeout(TIMEOUT, handle).await.unwrap().unwrap().unwrap(); + assert_eq!(result.interrupted, interrupted); + } +} + +#[tokio::test] +async fn interrupt_main_turn_propagates_method_not_found_without_abort_fallback() { + let (session, mut server) = create_session_pair().await; + let handle = tokio::spawn(async move { + session + .interrupt_main_turn(InterruptMainTurnOptions::default()) + .await + }); + + let request = server.read_request().await; + assert_eq!(request["method"], "session.interruptMainTurn"); + server + .respond_error(&request, -32601, "Method not found") + .await; + + let error = timeout(TIMEOUT, handle) + .await + .unwrap() + .unwrap() + .unwrap_err(); + assert_eq!(error.rpc_code(), Some(-32601)); + + let fallback = timeout(Duration::from_millis(100), server.read_request()).await; + assert!( + fallback.is_err(), + "session.interruptMainTurn must not fall back to session.abort" + ); +} + #[tokio::test] async fn client_rpc_methods_send_correct_method_names() { let (client, mut server_read, mut server_write) = make_client(); @@ -3134,6 +3232,7 @@ async fn capabilities_captured_from_create_response() { "result": { "sessionId": session_id, "capabilities": { + "interruptMainTurn": true, "ui": { "elicitation": true } } }, @@ -3142,40 +3241,52 @@ async fn capabilities_captured_from_create_response() { let session = timeout(TIMEOUT, create_handle).await.unwrap().unwrap(); let caps = session.capabilities(); + assert_eq!(caps.interrupt_main_turn, Some(true)); assert_eq!(caps.ui.as_ref().unwrap().elicitation, Some(true)); } #[tokio::test] -async fn capabilities_changed_event_updates_session() { - let (session, mut server) = create_session_pair().await; +async fn missing_interrupt_capability_from_older_server_remains_unknown() { + let (session, _server) = create_session_pair_with_capabilities(serde_json::json!({ + "ui": { "elicitation": true } + })) + .await; - // Initially no capabilities (create_session_pair doesn't send them) - assert!(session.capabilities().ui.is_none()); + assert_eq!(session.capabilities().interrupt_main_turn, None); +} + +#[tokio::test] +async fn capabilities_changed_event_replaces_complete_snapshot_and_is_typed() { + let (session, mut server) = create_session_pair_with_capabilities(serde_json::json!({ + "interruptMainTurn": true, + "ui": { "elicitation": true } + })) + .await; + let mut events = session.subscribe(); - // CLI sends capabilities.changed event server .send_event( "capabilities.changed", - serde_json::json!({ - "ui": { "elicitation": true } - }), + serde_json::json!({ "interruptMainTurn": false }), ) .await; - // Poll until the event loop processes the notification - let caps = timeout(TIMEOUT, async { - loop { - let caps = session.capabilities(); - if caps.ui.is_some() { - return caps; - } - tokio::time::sleep(Duration::from_millis(5)).await; - } - }) - .await - .expect("capabilities should update within timeout"); + let event = timeout(TIMEOUT, events.recv()) + .await + .expect("capabilities.changed should arrive within timeout") + .unwrap(); + let changed = event + .typed_data::() + .expect("capabilities.changed should expose typed data"); + assert_eq!(changed.interrupt_main_turn, Some(false)); + assert!(changed.ui.is_none()); - assert_eq!(caps.ui.as_ref().unwrap().elicitation, Some(true)); + let caps = session.capabilities(); + assert_eq!(caps.interrupt_main_turn, Some(false)); + assert!( + caps.ui.is_none(), + "each capabilities.changed payload is a complete replacement snapshot" + ); } #[tokio::test] @@ -3308,7 +3419,7 @@ async fn env_value_mode_hardcoded_direct_on_create_and_resume() { } #[tokio::test] -async fn resume_session_sends_canvas_fields_and_captures_open_canvases() { +async fn resume_session_sends_canvas_fields_and_captures_snapshots() { use github_copilot_sdk::types::ResumeSessionConfig; let (client, mut server_read, mut server_write) = make_client(); @@ -3372,6 +3483,7 @@ async fn resume_session_sends_canvas_fields_and_captures_open_canvases() { "url": "https://example.test/counter" }], "capabilities": { + "interruptMainTurn": false, "ui": { "canvases": true } } }, @@ -3389,6 +3501,7 @@ async fn resume_session_sends_canvas_fields_and_captures_open_canvases() { assert_eq!(open.len(), 1); assert_eq!(open[0].instance_id, "counter-1"); let caps = session.capabilities(); + assert_eq!(caps.interrupt_main_turn, Some(false)); assert_eq!(caps.ui.unwrap().canvases, Some(true)); }