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
15 changes: 13 additions & 2 deletions rust/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down
60 changes: 57 additions & 3 deletions rust/src/generated/api_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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
///
/// <div class="warning">
///
Expand All @@ -603,7 +605,7 @@ pub struct AbortRequest {
pub reason: Option<AbortReason>,
}

/// Result of aborting the current turn
/// Result of recursively aborting the active session work
///
/// <div class="warning">
///
Expand Down Expand Up @@ -4314,6 +4316,37 @@ pub struct InstructionsGetSourcesResult {
pub sources: Vec<InstructionSource>,
}

/// Parameters for interrupting only the active main coordinator turn while preserving background work
///
/// <div class="warning">
///
/// **Experimental.** This type is part of an experimental wire-protocol surface
/// and may change or be removed in future SDK or CLI releases.
///
/// </div>
#[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<bool>,
}

/// Result of interrupting only the active main coordinator turn
///
/// <div class="warning">
///
/// **Experimental.** This type is part of an experimental wire-protocol surface
/// and may change or be removed in future SDK or CLI releases.
///
/// </div>
#[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")]
Expand Down Expand Up @@ -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<String>,
/// Request count and cost metrics for this model
pub requests: UsageMetricsModelMetricRequests,
/// Token count details per type
Expand Down Expand Up @@ -16278,7 +16314,7 @@ pub struct SessionSendMessagesResult {
pub message_ids: Vec<String>,
}

/// Result of aborting the current turn
/// Result of recursively aborting the active session work
///
/// <div class="warning">
///
Expand All @@ -16296,6 +16332,21 @@ pub struct SessionAbortResult {
pub success: bool,
}

/// Result of interrupting only the active main coordinator turn
///
/// <div class="warning">
///
/// **Experimental.** This type is part of an experimental wire-protocol surface
/// and may change or be removed in future SDK or CLI releases.
///
/// </div>
#[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.
///
/// <div class="warning">
Expand Down Expand Up @@ -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,
Expand Down
39 changes: 36 additions & 3 deletions rust/src/generated/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
/// <div class="warning">
///
Expand All @@ -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
///
/// <div class="warning">
///
/// **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.
///
/// </div>
pub async fn interrupt_main_turn(
&self,
params: InterruptMainTurnRequest,
) -> Result<InterruptMainTurnResult, Error> {
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`.
Expand Down
23 changes: 23 additions & 0 deletions rust/src/generated/session_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1209,10 +1209,27 @@ pub struct SessionShutdownData {
pub(crate) total_premium_requests: Option<f64>,
}

/// 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<Vec<UsageCheckpointModelCacheState>>,
/// 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
Expand Down Expand Up @@ -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<AssistantUsageApiEndpoint>,
/// 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<String>,
/// Number of tokens read from prompt cache
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_read_tokens: Option<i64>,
Expand Down Expand Up @@ -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<bool>,
/// UI capability changes
#[serde(skip_serializing_if = "Option::is_none")]
pub ui: Option<CapabilitiesChangedUI>,
Expand Down
49 changes: 42 additions & 7 deletions rust/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
};
Expand Down Expand Up @@ -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
///
Expand All @@ -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<InterruptMainTurnResult, Error> {
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.
Expand Down Expand Up @@ -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::<SessionCapabilities>(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"),
}
}
Expand Down
26 changes: 26 additions & 0 deletions rust/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool>,
/// UI capabilities (elicitation support, etc.).
#[serde(skip_serializing_if = "Option::is_none")]
pub ui: Option<UiCapabilities>,
}

/// 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")]
Expand Down
Loading
Loading