@@ -2,16 +2,16 @@ use super::rendering::render_exec_response_for_assistant;
22use crate :: agentic:: tools:: framework:: { Tool , ToolResult , ToolUseContext , ValidationResult } ;
33use crate :: service:: remote_ssh:: {
44 get_global_remote_exec_process_manager, RemoteExecControlAction , RemoteExecControlOrigin ,
5- RemoteExecControlRequest , RemoteExecSessionCompletion , RemoteExecSessionCompletionSource ,
6- RemoteExecSessionCompletionStatus ,
5+ RemoteExecControlRequest , RemoteExecError , RemoteExecSessionCompletion ,
6+ RemoteExecSessionCompletionSource , RemoteExecSessionCompletionStatus ,
77} ;
88use crate :: util:: errors:: { BitFunError , BitFunResult } ;
99use async_trait:: async_trait;
1010use serde_json:: { json, Value } ;
1111use terminal_core:: {
1212 get_global_exec_process_manager, LocalExecControlAction , LocalExecControlOrigin ,
1313 LocalExecControlRequest , LocalExecSessionCompletion , LocalExecSessionCompletionSource ,
14- LocalExecSessionCompletionStatus ,
14+ LocalExecSessionCompletionStatus , TerminalError ,
1515} ;
1616
1717const DEFAULT_MAX_OUTPUT_CHARS : u64 = 10_000 ;
@@ -93,9 +93,18 @@ pub struct ExecCommandControlResponse {
9393 pub completion : Option < ExecCommandCompletion > ,
9494}
9595
96+ #[ derive( Debug , thiserror:: Error ) ]
97+ pub enum ExecCommandControlError {
98+ #[ error( "session not found: {0}" ) ]
99+ SessionNotFound ( i32 ) ,
100+
101+ #[ error( transparent) ]
102+ Tool ( #[ from] BitFunError ) ,
103+ }
104+
96105pub async fn control_exec_command_session (
97106 request : ExecCommandControlRequest ,
98- ) -> BitFunResult < ExecCommandControlResponse > {
107+ ) -> Result < ExecCommandControlResponse , ExecCommandControlError > {
99108 if request. remote {
100109 let response = get_global_remote_exec_process_manager ( )
101110 . control_session ( RemoteExecControlRequest {
@@ -106,7 +115,14 @@ pub async fn control_exec_command_session(
106115 max_output_chars : request. max_output_chars ,
107116 } )
108117 . await
109- . map_err ( |error| BitFunError :: tool ( format ! ( "ExecControl failed: {error}" ) ) ) ?;
118+ . map_err ( |error| match error {
119+ RemoteExecError :: SessionNotFound ( session_id) => {
120+ ExecCommandControlError :: SessionNotFound ( session_id)
121+ }
122+ error => ExecCommandControlError :: Tool ( BitFunError :: tool ( format ! (
123+ "ExecControl failed: {error}"
124+ ) ) ) ,
125+ } ) ?;
110126
111127 return Ok ( ExecCommandControlResponse {
112128 chunk_id : response. chunk_id ,
@@ -130,7 +146,14 @@ pub async fn control_exec_command_session(
130146 max_output_chars : request. max_output_chars ,
131147 } )
132148 . await
133- . map_err ( |error| BitFunError :: tool ( format ! ( "ExecControl failed: {error}" ) ) ) ?;
149+ . map_err ( |error| match error {
150+ TerminalError :: SessionNotFound ( _) => {
151+ ExecCommandControlError :: SessionNotFound ( request. session_id )
152+ }
153+ error => ExecCommandControlError :: Tool ( BitFunError :: tool ( format ! (
154+ "ExecControl failed: {error}"
155+ ) ) ) ,
156+ } ) ?;
134157
135158 Ok ( ExecCommandControlResponse {
136159 chunk_id : response. chunk_id ,
@@ -193,6 +216,39 @@ impl ExecControlTool {
193216 render_exec_response_for_assistant ( data, status_lines, 4 )
194217 }
195218
219+ fn session_not_found_result (
220+ session_id : i32 ,
221+ action : ExecCommandControlAction ,
222+ remote : bool ,
223+ ) -> Vec < ToolResult > {
224+ let action_name = match action {
225+ ExecCommandControlAction :: Interrupt => "interrupt" ,
226+ ExecCommandControlAction :: Kill => "kill" ,
227+ } ;
228+ let message = format ! (
229+ "No {action_name} was sent because ExecCommand session {session_id} was not found. It may have already exited, been collected, or been pruned."
230+ ) ;
231+ let mut data = json ! ( {
232+ "status" : "session_not_found" ,
233+ "message" : message,
234+ "requested_session_id" : session_id,
235+ "session_id" : null,
236+ "exit_code" : null,
237+ "output" : "" ,
238+ "original_output_chars" : 0 ,
239+ "action" : action_name,
240+ } ) ;
241+ if remote {
242+ data[ "remote" ] = json ! ( true ) ;
243+ }
244+
245+ vec ! [ ToolResult :: Result {
246+ data,
247+ result_for_assistant: Some ( message) ,
248+ image_attachments: None ,
249+ } ]
250+ }
251+
196252 fn local_action ( action : ExecCommandControlAction ) -> LocalExecControlAction {
197253 match action {
198254 ExecCommandControlAction :: Interrupt => LocalExecControlAction :: Interrupt ,
@@ -274,15 +330,22 @@ impl ExecControlTool {
274330 . try_into ( )
275331 . unwrap_or ( usize:: MAX ) ;
276332
277- let response = control_exec_command_session ( ExecCommandControlRequest {
333+ let response = match control_exec_command_session ( ExecCommandControlRequest {
278334 session_id,
279335 action,
280336 origin : ExecCommandControlOrigin :: ModelTool ,
281337 remote : true ,
282338 yield_time_ms,
283339 max_output_chars : Some ( max_output_chars) ,
284340 } )
285- . await ?;
341+ . await
342+ {
343+ Ok ( response) => response,
344+ Err ( ExecCommandControlError :: SessionNotFound ( session_id) ) => {
345+ return Ok ( Self :: session_not_found_result ( session_id, action, true ) ) ;
346+ }
347+ Err ( ExecCommandControlError :: Tool ( error) ) => return Err ( error) ,
348+ } ;
286349
287350 let action_name = match action {
288351 ExecCommandControlAction :: Interrupt => "interrupt" ,
@@ -422,15 +485,22 @@ After the action, yield_time_ms waits for output or exit status. Output is only
422485 . try_into ( )
423486 . unwrap_or ( usize:: MAX ) ;
424487
425- let response = control_exec_command_session ( ExecCommandControlRequest {
488+ let response = match control_exec_command_session ( ExecCommandControlRequest {
426489 session_id,
427490 action,
428491 origin : ExecCommandControlOrigin :: ModelTool ,
429492 remote : false ,
430493 yield_time_ms,
431494 max_output_chars : Some ( max_output_chars) ,
432495 } )
433- . await ?;
496+ . await
497+ {
498+ Ok ( response) => response,
499+ Err ( ExecCommandControlError :: SessionNotFound ( session_id) ) => {
500+ return Ok ( Self :: session_not_found_result ( session_id, action, false ) ) ;
501+ }
502+ Err ( ExecCommandControlError :: Tool ( error) ) => return Err ( error) ,
503+ } ;
434504
435505 let action_name = match action {
436506 ExecCommandControlAction :: Interrupt => "interrupt" ,
@@ -454,3 +524,63 @@ After the action, yield_time_ms waits for output or exit status. Output is only
454524 } ] )
455525 }
456526}
527+
528+ #[ cfg( test) ]
529+ mod tests {
530+ use super :: {
531+ control_exec_command_session, ExecCommandControlAction , ExecCommandControlError ,
532+ ExecCommandControlOrigin , ExecCommandControlRequest , ExecControlTool ,
533+ } ;
534+ use crate :: agentic:: tools:: framework:: ToolResult ;
535+
536+ #[ test]
537+ fn session_not_found_result_uses_plain_assistant_message ( ) {
538+ let results = ExecControlTool :: session_not_found_result (
539+ 456 ,
540+ ExecCommandControlAction :: Interrupt ,
541+ false ,
542+ ) ;
543+ let ToolResult :: Result {
544+ data,
545+ result_for_assistant,
546+ ..
547+ } = & results[ 0 ]
548+ else {
549+ panic ! ( "expected result" ) ;
550+ } ;
551+
552+ assert_eq ! (
553+ data. get( "status" ) . and_then( |value| value. as_str( ) ) ,
554+ Some ( "session_not_found" )
555+ ) ;
556+ assert_eq ! (
557+ data. get( "requested_session_id" )
558+ . and_then( |value| value. as_i64( ) ) ,
559+ Some ( 456 )
560+ ) ;
561+ let assistant = result_for_assistant. as_deref ( ) . expect ( "assistant text" ) ;
562+ assert ! ( assistant. contains( "No interrupt was sent" ) ) ;
563+ assert ! ( assistant. contains( "ExecCommand session 456 was not found" ) ) ;
564+ assert ! ( !assistant. contains( "<wall_time>" ) ) ;
565+ assert ! ( !assistant. contains( "<output>" ) ) ;
566+ }
567+
568+ #[ tokio:: test]
569+ async fn control_exec_command_session_returns_structured_session_not_found ( ) {
570+ let error = control_exec_command_session ( ExecCommandControlRequest {
571+ session_id : 987_654 ,
572+ action : ExecCommandControlAction :: Kill ,
573+ origin : ExecCommandControlOrigin :: ModelTool ,
574+ remote : false ,
575+ yield_time_ms : Some ( 0 ) ,
576+ max_output_chars : Some ( 1 ) ,
577+ } )
578+ . await
579+ . expect_err ( "missing session should be structured" ) ;
580+
581+ assert ! ( matches!(
582+ error,
583+ ExecCommandControlError :: SessionNotFound ( 987_654 )
584+ ) ) ;
585+ }
586+ }
0 commit comments