@@ -16,12 +16,13 @@ use crate::runtime::{
1616use crate :: startup_trace:: DesktopStartupTrace ;
1717use bitfun_agent_runtime:: deep_review:: sanitize_focused_review_public_metadata;
1818use bitfun_agent_runtime:: sdk:: {
19- AgentDialogSteerRequest , AgentDialogTurnExecution , AgentDialogTurnRequest ,
20- AgentInputAttachment , AgentSessionCreateResult , AgentSessionModeUpdateRequest ,
21- AgentSessionModelSelection , AgentSessionModelSelectionUpdateRequest ,
22- AgentSessionModelUpdateRequest , AgentSubmissionSource , AgentTurnCancellationRequest ,
23- DialogSteerOutcome , PermissionAuditRecord , PermissionGrant , PermissionGrantKey ,
24- PermissionReply , PermissionRequest ,
19+ AgentDialogSteerRequest , AgentDialogTurnExecution , AgentDialogTurnRecoveryOutcome ,
20+ AgentDialogTurnRecoveryRequest , AgentDialogTurnRequest , AgentInputAttachment ,
21+ AgentSessionCreateResult , AgentSessionModeUpdateRequest , AgentSessionModelSelection ,
22+ AgentSessionModelSelectionUpdateRequest , AgentSessionModelUpdateRequest , AgentSubmissionSource ,
23+ AgentTurnCancellationRequest , AgentTurnInterruptionRequest , DialogSteerOutcome ,
24+ PermissionAuditRecord , PermissionGrant , PermissionGrantKey , PermissionReply , PermissionRequest ,
25+ RuntimeError ,
2526} ;
2627use bitfun_core:: agentic:: agents:: AgentSource ;
2728use bitfun_core:: agentic:: coordination:: {
@@ -65,7 +66,7 @@ use bitfun_core_types::{
6566 WorktreeError , WorktreeErrorCode ,
6667} ;
6768use bitfun_product_domains:: tool_permissions:: PermissionRule ;
68- use bitfun_runtime_ports:: { PermissionMode , SessionTurnWindowRequest } ;
69+ use bitfun_runtime_ports:: { PermissionMode , PortErrorKind , SessionTurnWindowRequest } ;
6970
7071const SESSION_VIEW_TOOL_RESULT_TOTAL_CHAR_BUDGET : usize = 512 * 1024 ;
7172const SESSION_VIEW_TOOL_RESULT_STRING_CHAR_LIMIT : usize = 16 * 1024 ;
@@ -740,6 +741,20 @@ pub struct CancelDialogTurnRequest {
740741 pub dialog_turn_id : String ,
741742}
742743
744+ #[ derive( Debug , Deserialize ) ]
745+ #[ serde( rename_all = "camelCase" ) ]
746+ pub struct RecoverInterruptedDialogTurnRequest {
747+ pub session_id : String ,
748+ pub dialog_turn_id : String ,
749+ pub execution_generation : u32 ,
750+ #[ serde( default ) ]
751+ pub workspace_path : Option < String > ,
752+ #[ serde( default ) ]
753+ pub remote_connection_id : Option < String > ,
754+ #[ serde( default ) ]
755+ pub remote_ssh_host : Option < String > ,
756+ }
757+
743758#[ derive( Debug , Deserialize ) ]
744759#[ serde( rename_all = "camelCase" ) ]
745760pub struct SteerDialogTurnRequest {
@@ -2747,6 +2762,97 @@ pub async fn cancel_dialog_turn(
27472762 . map ( |_| ( ) )
27482763}
27492764
2765+ /// Request a recoverable interruption for a native local dialog turn. The
2766+ /// command returns only after the old execution has settled and the
2767+ /// `dialog-turn-interrupted` event is safe to act on.
2768+ #[ tauri:: command]
2769+ pub async fn interrupt_dialog_turn (
2770+ runtime : State < ' _ , DesktopRuntimeContext > ,
2771+ request : CancelDialogTurnRequest ,
2772+ ) -> Result < ( ) , String > {
2773+ let interruption = runtime
2774+ . agent_runtime ( )
2775+ . interrupt_turn ( AgentTurnInterruptionRequest {
2776+ session_id : request. session_id . clone ( ) ,
2777+ turn_id : request. dialog_turn_id . clone ( ) ,
2778+ source : Some ( AgentSubmissionSource :: DesktopUi ) ,
2779+ wait_timeout_ms : Some ( 30_000 ) ,
2780+ } )
2781+ . await ;
2782+ match interruption {
2783+ Ok ( _) => Ok ( ( ) ) ,
2784+ // Stop remains universally available. Unsupported recovery surfaces
2785+ // (external routes, Goal, remote, non-standard sessions) degrade to
2786+ // the existing hard cancellation and emit DialogTurnCancelled.
2787+ Err ( RuntimeError :: Port ( error) )
2788+ if matches ! ( error. kind, PortErrorKind :: InvalidRequest | PortErrorKind :: Timeout ) => runtime
2789+ . agent_runtime ( )
2790+ . cancel_turn ( AgentTurnCancellationRequest {
2791+ session_id : request. session_id . clone ( ) ,
2792+ turn_id : Some ( request. dialog_turn_id . clone ( ) ) ,
2793+ source : Some ( AgentSubmissionSource :: DesktopUi ) ,
2794+ requester_session_id : None ,
2795+ reason : Some ( if error. kind == PortErrorKind :: Timeout {
2796+ "recoverable interruption timed out" . to_string ( )
2797+ } else {
2798+ "recoverable interruption unavailable" . to_string ( )
2799+ } ) ,
2800+ wait_timeout_ms : None ,
2801+ cancel_descendants : true ,
2802+ } )
2803+ . await
2804+ . map ( |_| ( ) )
2805+ . map_err ( |fallback_error| {
2806+ log:: error!(
2807+ "Failed to cancel dialog turn after interruption was unavailable: session_id={}, dialog_turn_id={}, error={}" ,
2808+ request. session_id,
2809+ request. dialog_turn_id,
2810+ fallback_error
2811+ ) ;
2812+ format ! ( "Failed to cancel dialog turn: {}" , fallback_error. into_message( ) )
2813+ } ) ,
2814+ Err ( error) => {
2815+ log:: error!(
2816+ "Failed to interrupt dialog turn: session_id={}, dialog_turn_id={}, error={}" ,
2817+ request. session_id,
2818+ request. dialog_turn_id,
2819+ error
2820+ ) ;
2821+ Err ( format ! ( "Failed to interrupt dialog turn: {}" , error. into_message( ) ) )
2822+ }
2823+ }
2824+ }
2825+
2826+ #[ tauri:: command]
2827+ pub async fn recover_interrupted_dialog_turn (
2828+ runtime : State < ' _ , DesktopRuntimeContext > ,
2829+ request : RecoverInterruptedDialogTurnRequest ,
2830+ ) -> Result < AgentDialogTurnRecoveryOutcome , String > {
2831+ runtime
2832+ . agent_runtime ( )
2833+ . recover_interrupted_turn ( AgentDialogTurnRecoveryRequest {
2834+ session_id : request. session_id . clone ( ) ,
2835+ turn_id : request. dialog_turn_id . clone ( ) ,
2836+ execution_generation : request. execution_generation ,
2837+ workspace_path : request. workspace_path ,
2838+ remote_connection_id : request. remote_connection_id ,
2839+ remote_ssh_host : request. remote_ssh_host ,
2840+ } )
2841+ . await
2842+ . map_err ( |error| {
2843+ log:: error!(
2844+ "Failed to recover interrupted dialog turn: session_id={}, dialog_turn_id={}, error={}" ,
2845+ request. session_id,
2846+ request. dialog_turn_id,
2847+ error
2848+ ) ;
2849+ format ! (
2850+ "Failed to recover interrupted dialog turn: {}" ,
2851+ error. into_message( )
2852+ )
2853+ } )
2854+ }
2855+
27502856#[ tauri:: command]
27512857pub async fn steer_dialog_turn (
27522858 runtime : State < ' _ , DesktopRuntimeContext > ,
@@ -4142,6 +4248,8 @@ mod tests {
41424248 token_usage : None ,
41434249 finish_reason : None ,
41444250 has_final_response : None ,
4251+ recovery : None ,
4252+ recovery_epoch : None ,
41454253 error : None ,
41464254 error_detail : None ,
41474255 status : TurnStatus :: Completed ,
@@ -4226,6 +4334,8 @@ mod tests {
42264334 token_usage: None ,
42274335 finish_reason: None ,
42284336 has_final_response: None ,
4337+ recovery: None ,
4338+ recovery_epoch: None ,
42294339 error: None ,
42304340 error_detail: None ,
42314341 status: TurnStatus :: Completed ,
@@ -4291,6 +4401,8 @@ mod tests {
42914401 token_usage: None ,
42924402 finish_reason: None ,
42934403 has_final_response: None ,
4404+ recovery: None ,
4405+ recovery_epoch: None ,
42944406 error: None ,
42954407 error_detail: None ,
42964408 status: TurnStatus :: Completed ,
0 commit comments