Skip to content
Closed
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
11 changes: 9 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
// Stop only the foreground turn while preserving background work.
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
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
43 changes: 37 additions & 6 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 its descendant and
/// background work.
///
/// Use [`interrupt_main_turn`](Self::interrupt_main_turn) when background
/// work must survive.
///
/// # Cancel safety
///
Expand All @@ -526,6 +530,33 @@ impl Session {
Ok(())
}

/// Interrupt only the active main coordinator turn while preserving
/// background agents and other background work.
///
/// The default `flush_queued: false` discards queued prompts. Set
/// `flush_queued: true` to preserve queued user prompts for the next
/// eligible turn while dropping hidden system prompts.
///
/// A result with `interrupted == false` means the method is supported but
/// no main turn was active. Older CLIs and unsupported remote sessions
/// return JSON-RPC `-32601`; that error is propagated without falling back
/// to recursive [`abort`](Self::abort).
///
/// # Cancel safety
///
/// **Cancel-safe.** A single `session.interruptMainTurn` RPC is dispatched
/// through 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
25 changes: 25 additions & 0 deletions rust/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5235,12 +5235,37 @@ 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 the host supports scoped interruption via
/// [`Session::interrupt_main_turn`](crate::session::Session::interrupt_main_turn).
///
/// 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 while dropping
/// hidden system prompts. The default (`false`) discards queued prompts.
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
14 changes: 7 additions & 7 deletions rust/tests/e2e/elicitation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,20 +380,20 @@ 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),
Some(true)
);

let empty = github_copilot_sdk::SessionCapabilities::default();
assert!(empty.interrupt_main_turn.is_none());
assert!(empty.ui.is_none());
}

Expand Down
Loading
Loading