@@ -467,10 +467,30 @@ fn extract_tool_session_messages(
467467 }
468468}
469469
470- fn has_tool_result_blocks ( blocks : & [ ContentBlock ] ) -> bool {
470+ fn count_tool_use_blocks ( blocks : & [ ContentBlock ] ) -> usize {
471471 blocks
472472 . iter ( )
473- . any ( |b| matches ! ( b, ContentBlock :: ToolResult ( _) ) )
473+ . filter ( |block| matches ! ( block, ContentBlock :: ToolUse ( _) ) )
474+ . count ( )
475+ }
476+
477+ fn count_tool_result_blocks ( blocks : & [ ContentBlock ] ) -> usize {
478+ blocks
479+ . iter ( )
480+ . filter ( |block| matches ! ( block, ContentBlock :: ToolResult ( _) ) )
481+ . count ( )
482+ }
483+
484+ fn count_user_tool_result_messages ( user_msg : & claude_agent_sdk:: UserMessage ) -> usize {
485+ let block_count = match user_msg. content {
486+ claude_agent_sdk:: UserContent :: Blocks ( ref blocks) => count_tool_result_blocks ( blocks) ,
487+ claude_agent_sdk:: UserContent :: Text ( _) => 0 ,
488+ } ;
489+ if block_count > 0 {
490+ block_count
491+ } else {
492+ usize:: from ( user_msg. tool_use_result . is_some ( ) )
493+ }
474494}
475495
476496fn metadata_string_map (
@@ -643,9 +663,13 @@ pub struct ClaudeCliProvider {
643663 run_session_map : Mutex < HashMap < String , String > > ,
644664 /// User inputs accepted but not yet completed per live run.
645665 run_pending_inputs : Mutex < HashMap < String , VecDeque < PendingAckMarker > > > ,
666+ /// User inputs accepted while Claude is between tool_use and tool_result.
667+ run_input_states : Mutex < HashMap < String , RunInputState > > ,
646668 /// Last user message per thread, used for auto-recovery replay.
647669 last_messages : Mutex < HashMap < String , String > > ,
648670 #[ cfg( test) ]
671+ test_sent_streaming_inputs : Mutex < Vec < ( String , OutboundUserMessage ) > > ,
672+ #[ cfg( test) ]
649673 test_run_attempts : Mutex < VecDeque < Result < Option < SdkRunOutcome > , BridgeError > > > ,
650674 #[ cfg( test) ]
651675 test_recorded_session_attempts : Mutex < Vec < Option < String > > > ,
@@ -658,6 +682,18 @@ enum PendingAckMarker {
658682 QueuedInput ( String ) ,
659683}
660684
685+ #[ derive( Debug , Clone ) ]
686+ struct PendingClaudeInput {
687+ pending_input_id : String ,
688+ outbound : OutboundUserMessage ,
689+ }
690+
691+ #[ derive( Debug , Default ) ]
692+ struct RunInputState {
693+ outstanding_tool_uses : usize ,
694+ queued_inputs : VecDeque < PendingClaudeInput > ,
695+ }
696+
661697impl ClaudeCliProvider {
662698 /// Create a new provider with the given config.
663699 pub fn new ( config : ClaudeCodeConfig ) -> Self {
@@ -668,8 +704,11 @@ impl ClaudeCliProvider {
668704 active_runs : Mutex :: new ( HashMap :: new ( ) ) ,
669705 run_session_map : Mutex :: new ( HashMap :: new ( ) ) ,
670706 run_pending_inputs : Mutex :: new ( HashMap :: new ( ) ) ,
707+ run_input_states : Mutex :: new ( HashMap :: new ( ) ) ,
671708 last_messages : Mutex :: new ( HashMap :: new ( ) ) ,
672709 #[ cfg( test) ]
710+ test_sent_streaming_inputs : Mutex :: new ( Vec :: new ( ) ) ,
711+ #[ cfg( test) ]
673712 test_run_attempts : Mutex :: new ( VecDeque :: new ( ) ) ,
674713 #[ cfg( test) ]
675714 test_recorded_session_attempts : Mutex :: new ( Vec :: new ( ) ) ,
@@ -881,13 +920,18 @@ impl ClaudeCliProvider {
881920 . lock ( )
882921 . await
883922 . insert ( run_id. to_owned ( ) , thread_id. to_owned ( ) ) ;
923+ self . run_input_states
924+ . lock ( )
925+ . await
926+ . insert ( run_id. to_owned ( ) , RunInputState :: default ( ) ) ;
884927 }
885928
886929 /// Unregister a run after completion and return handle for cleanup.
887930 async fn unregister_run ( & self , run_id : & str ) -> ( Option < ClaudeRunControl > , Option < String > ) {
888931 let run = self . active_runs . lock ( ) . await . remove ( run_id) ;
889932 let thread_id = self . run_session_map . lock ( ) . await . remove ( run_id) ;
890933 self . run_pending_inputs . lock ( ) . await . remove ( run_id) ;
934+ self . run_input_states . lock ( ) . await . remove ( run_id) ;
891935 ( run, thread_id)
892936 }
893937
@@ -898,6 +942,124 @@ impl ClaudeCliProvider {
898942 }
899943 }
900944
945+ async fn note_tool_uses_started ( & self , run_id : & str , count : usize ) {
946+ if count == 0 {
947+ return ;
948+ }
949+ if let Some ( state) = self . run_input_states . lock ( ) . await . get_mut ( run_id) {
950+ state. outstanding_tool_uses = state. outstanding_tool_uses . saturating_add ( count) ;
951+ }
952+ }
953+
954+ async fn note_tool_results_finished ( & self , run_id : & str , count : usize ) {
955+ if count == 0 {
956+ return ;
957+ }
958+ if let Some ( state) = self . run_input_states . lock ( ) . await . get_mut ( run_id) {
959+ state. outstanding_tool_uses = state. outstanding_tool_uses . saturating_sub ( count) ;
960+ }
961+ }
962+
963+ async fn send_streaming_input_now (
964+ & self ,
965+ run_id : & str ,
966+ pending_input_id : & str ,
967+ outbound : OutboundUserMessage ,
968+ ) -> bool {
969+ let run = { self . active_runs . lock ( ) . await . get ( run_id) . cloned ( ) } ;
970+ if let Some ( run) = run {
971+ match run. send_user_message ( outbound) . await {
972+ Ok ( ( ) ) => return true ,
973+ Err ( e) => {
974+ self . rollback_pending_input ( run_id, pending_input_id) . await ;
975+ tracing:: warn!(
976+ run_id = %run_id,
977+ pending_input_id = %pending_input_id,
978+ error = %e,
979+ "failed to send queued Claude streaming input"
980+ ) ;
981+ return false ;
982+ }
983+ }
984+ }
985+
986+ #[ cfg( test) ]
987+ {
988+ self . test_sent_streaming_inputs
989+ . lock ( )
990+ . await
991+ . push ( ( run_id. to_owned ( ) , outbound) ) ;
992+ return true ;
993+ }
994+
995+ #[ cfg( not( test) ) ]
996+ {
997+ self . rollback_pending_input ( run_id, pending_input_id) . await ;
998+ false
999+ }
1000+ }
1001+
1002+ async fn send_or_queue_streaming_input (
1003+ & self ,
1004+ run_id : & str ,
1005+ pending_input_id : String ,
1006+ outbound : OutboundUserMessage ,
1007+ ) -> bool {
1008+ let pending = PendingClaudeInput {
1009+ pending_input_id,
1010+ outbound,
1011+ } ;
1012+
1013+ {
1014+ let mut states = self . run_input_states . lock ( ) . await ;
1015+ if let Some ( state) = states. get_mut ( run_id)
1016+ && state. outstanding_tool_uses > 0
1017+ {
1018+ state. queued_inputs . push_back ( pending) ;
1019+ tracing:: debug!(
1020+ run_id = %run_id,
1021+ outstanding_tool_uses = state. outstanding_tool_uses,
1022+ queued_inputs = state. queued_inputs. len( ) ,
1023+ "deferring Claude streaming input until tool results are complete"
1024+ ) ;
1025+ return true ;
1026+ }
1027+ }
1028+
1029+ let PendingClaudeInput {
1030+ pending_input_id,
1031+ outbound,
1032+ } = pending;
1033+ self . send_streaming_input_now ( run_id, & pending_input_id, outbound)
1034+ . await
1035+ }
1036+
1037+ async fn flush_queued_streaming_inputs_if_safe ( & self , run_id : & str ) {
1038+ loop {
1039+ let pending = {
1040+ let mut states = self . run_input_states . lock ( ) . await ;
1041+ let Some ( state) = states. get_mut ( run_id) else {
1042+ return ;
1043+ } ;
1044+ if state. outstanding_tool_uses > 0 {
1045+ return ;
1046+ }
1047+ state. queued_inputs . pop_front ( )
1048+ } ;
1049+
1050+ let Some ( pending) = pending else {
1051+ return ;
1052+ } ;
1053+
1054+ let sent = self
1055+ . send_streaming_input_now ( run_id, & pending. pending_input_id , pending. outbound )
1056+ . await ;
1057+ if !sent {
1058+ return ;
1059+ }
1060+ }
1061+ }
1062+
9011063 async fn initialize_pending_inputs ( & self , run_id : & str ) {
9021064 self . run_pending_inputs . lock ( ) . await . insert (
9031065 run_id. to_owned ( ) ,
@@ -1187,6 +1349,7 @@ impl ClaudeCliProvider {
11871349 Ok ( None ) => break , // stream closed normally
11881350 Ok ( Some ( msg_result) ) => match msg_result {
11891351 Ok ( Message :: User ( user_msg) ) => {
1352+ let tool_result_count = count_user_tool_result_messages ( & user_msg) ;
11901353 if let claude_agent_sdk:: UserContent :: Blocks ( ref blocks) = user_msg. content
11911354 {
11921355 extract_tool_session_messages (
@@ -1195,10 +1358,12 @@ impl ClaudeCliProvider {
11951358 Some ( on_chunk) ,
11961359 user_msg. parent_tool_use_id . as_deref ( ) ,
11971360 ) ;
1198- if has_tool_result_blocks ( blocks) || user_msg. tool_use_result . is_some ( )
1199- {
1200- continue ;
1201- }
1361+ }
1362+ if tool_result_count > 0 {
1363+ self . note_tool_results_finished ( run_id, tool_result_count)
1364+ . await ;
1365+ self . flush_queued_streaming_inputs_if_safe ( run_id) . await ;
1366+ continue ;
12021367 }
12031368 // Keep Python parity: normal UserMessage echoes are ACKs that
12041369 // indicate one queued user input has been consumed.
@@ -1215,13 +1380,18 @@ impl ClaudeCliProvider {
12151380 Ok ( Message :: Assistant ( assistant_msg) ) => {
12161381 if is_synthetic_no_response_message ( & assistant_msg) {
12171382 tracing:: debug!(
1218- run_id = %run_id,
1219- thread_id = %thread_id,
1383+ run_id = %run_id,
1384+ thread_id = %thread_id,
12201385 "suppressing claude synthetic no-response placeholder"
12211386 ) ;
12221387 continue ;
12231388 }
12241389 assistant_or_tool_activity_seen = true ;
1390+ self . note_tool_uses_started (
1391+ run_id,
1392+ count_tool_use_blocks ( & assistant_msg. content ) ,
1393+ )
1394+ . await ;
12251395 if actual_model. is_none ( ) {
12261396 actual_model = Some ( assistant_msg. model . trim ( ) . to_owned ( ) )
12271397 . filter ( |value| !value. is_empty ( ) ) ;
@@ -1265,6 +1435,7 @@ impl ClaudeCliProvider {
12651435 thread_title : thread_title. clone ( ) ,
12661436 session_messages : session_messages. clone ( ) ,
12671437 } ) ;
1438+ self . flush_queued_streaming_inputs_if_safe ( run_id) . await ;
12681439 // Atomically check-and-close: if no queued inputs remain,
12691440 // remove the queue entry so that concurrent
12701441 // add_streaming_input callers see the run as closed and
@@ -1446,6 +1617,7 @@ impl AgentLoopProvider for ClaudeCliProvider {
14461617 } ;
14471618 self . run_session_map . lock ( ) . await . clear ( ) ;
14481619 self . run_pending_inputs . lock ( ) . await . clear ( ) ;
1620+ self . run_input_states . lock ( ) . await . clear ( ) ;
14491621
14501622 for ( run_id, run) in runs {
14511623 if let Err ( e) = run. interrupt ( ) . await {
@@ -1621,8 +1793,8 @@ impl AgentLoopProvider for ClaudeCliProvider {
16211793
16221794 async fn add_streaming_input ( & self , thread_id : & str , input : QueuedUserInput ) -> bool {
16231795 if let Some ( run_id) = self . resolve_active_run_id_for_session ( thread_id) . await {
1624- let run = { self . active_runs . lock ( ) . await . get ( & run_id) . cloned ( ) } ;
1625- if let Some ( run ) = run {
1796+ let has_run = { self . active_runs . lock ( ) . await . contains_key ( & run_id) } ;
1797+ if has_run {
16261798 let pending_input_id = input. pending_input_id . unwrap_or_default ( ) ;
16271799 if pending_input_id. trim ( ) . is_empty ( ) {
16281800 return false ;
@@ -1641,21 +1813,14 @@ impl AgentLoopProvider for ClaudeCliProvider {
16411813 UserInput :: Text ( text) => OutboundUserMessage :: text ( text, "" ) ,
16421814 UserInput :: Blocks ( blocks) => OutboundUserMessage :: blocks ( blocks, "" ) ,
16431815 } ;
1644- match run. send_user_message ( outbound) . await {
1645- Ok ( ( ) ) => {
1646- tracing:: debug!( thread_id = %thread_id, "queued streaming input" ) ;
1647- true
1648- }
1649- Err ( e) => {
1650- self . rollback_pending_input ( & run_id, & pending_input_id)
1651- . await ;
1652- tracing:: warn!(
1653- thread_id = %thread_id,
1654- error = %e,
1655- "failed to queue streaming input"
1656- ) ;
1657- false
1658- }
1816+ if self
1817+ . send_or_queue_streaming_input ( & run_id, pending_input_id. clone ( ) , outbound)
1818+ . await
1819+ {
1820+ tracing:: debug!( thread_id = %thread_id, "queued streaming input" ) ;
1821+ true
1822+ } else {
1823+ false
16591824 }
16601825 } else {
16611826 let _ = self . unregister_run ( & run_id) . await ;
0 commit comments