@@ -888,6 +888,43 @@ impl AgentLoop {
888888 . await
889889 }
890890
891+ /// Execute the agent loop with pre-built messages (user message already included).
892+ ///
893+ /// Used by `send_with_attachments` / `stream_with_attachments` where the
894+ /// user message contains multi-modal content and is already appended to
895+ /// the messages vec.
896+ pub async fn execute_from_messages (
897+ & self ,
898+ messages : Vec < Message > ,
899+ session_id : Option < & str > ,
900+ event_tx : Option < mpsc:: Sender < AgentEvent > > ,
901+ ) -> Result < AgentResult > {
902+ tracing:: info!(
903+ a3s. session. id = session_id. unwrap_or( "none" ) ,
904+ a3s. agent. max_turns = self . config. max_tool_rounds,
905+ "a3s.agent.execute_from_messages started"
906+ ) ;
907+
908+ // Pass empty prompt so execute_loop skips adding a user message
909+ let result = self
910+ . execute_loop ( & messages, "" , session_id, event_tx)
911+ . await ;
912+
913+ match & result {
914+ Ok ( r) => tracing:: info!(
915+ a3s. agent. tool_calls_count = r. tool_calls_count,
916+ a3s. llm. total_tokens = r. usage. total_tokens,
917+ "a3s.agent.execute_from_messages completed"
918+ ) ,
919+ Err ( e) => tracing:: warn!(
920+ error = %e,
921+ "a3s.agent.execute_from_messages failed"
922+ ) ,
923+ }
924+
925+ result
926+ }
927+
891928 /// Execute the agent loop for a prompt with session context
892929 ///
893930 /// Takes the conversation history, user prompt, and optional session ID.
@@ -1004,7 +1041,9 @@ impl AgentLoop {
10041041 } ;
10051042
10061043 // Add user message
1007- messages. push ( Message :: user ( prompt) ) ;
1044+ if !prompt. is_empty ( ) {
1045+ messages. push ( Message :: user ( prompt) ) ;
1046+ }
10081047
10091048 loop {
10101049 turn += 1 ;
@@ -1285,7 +1324,7 @@ impl AgentLoop {
12851324 PermissionDecision :: Ask
12861325 } ;
12871326
1288- let ( output, exit_code, is_error, _metadata) = match permission_decision {
1327+ let ( output, exit_code, is_error, _metadata, images ) = match permission_decision {
12891328 PermissionDecision :: Deny => {
12901329 tracing:: info!(
12911330 tool_name = tool_call. name. as_str( ) ,
@@ -1310,7 +1349,7 @@ impl AgentLoop {
13101349 . ok ( ) ;
13111350 }
13121351
1313- ( denial_msg, 1 , true , None )
1352+ ( denial_msg, 1 , true , None , Vec :: new ( ) )
13141353 }
13151354 PermissionDecision :: Allow => {
13161355 tracing:: info!(
@@ -1326,8 +1365,8 @@ impl AgentLoop {
13261365 . await ;
13271366
13281367 match result {
1329- Ok ( r) => ( r. output , r. exit_code , r. exit_code != 0 , r. metadata ) ,
1330- Err ( e) => ( format ! ( "Tool execution error: {}" , e) , 1 , true , None ) ,
1368+ Ok ( r) => ( r. output , r. exit_code , r. exit_code != 0 , r. metadata , r . images ) ,
1369+ Err ( e) => ( format ! ( "Tool execution error: {}" , e) , 1 , true , None , Vec :: new ( ) ) ,
13311370 }
13321371 }
13331372 PermissionDecision :: Ask => {
@@ -1353,19 +1392,28 @@ impl AgentLoop {
13531392 )
13541393 . await ;
13551394
1356- let ( output, exit_code, is_error, _metadata) = match result {
1357- Ok ( r) => ( r. output , r. exit_code , r. exit_code != 0 , r. metadata ) ,
1395+ let ( output, exit_code, is_error, _metadata, images ) = match result {
1396+ Ok ( r) => ( r. output , r. exit_code , r. exit_code != 0 , r. metadata , r . images ) ,
13581397 Err ( e) => {
1359- ( format ! ( "Tool execution error: {}" , e) , 1 , true , None )
1398+ ( format ! ( "Tool execution error: {}" , e) , 1 , true , None , Vec :: new ( ) )
13601399 }
13611400 } ;
13621401
13631402 // Add tool result to messages
1364- messages. push ( Message :: tool_result (
1365- & tool_call. id ,
1366- & output,
1367- is_error,
1368- ) ) ;
1403+ if images. is_empty ( ) {
1404+ messages. push ( Message :: tool_result (
1405+ & tool_call. id ,
1406+ & output,
1407+ is_error,
1408+ ) ) ;
1409+ } else {
1410+ messages. push ( Message :: tool_result_with_images (
1411+ & tool_call. id ,
1412+ & output,
1413+ & images,
1414+ is_error,
1415+ ) ) ;
1416+ }
13691417
13701418 // Record tool result on the tool span for early exit
13711419 let tool_duration = tool_start. elapsed ( ) ;
@@ -1425,12 +1473,14 @@ impl AgentLoop {
14251473 r. exit_code ,
14261474 r. exit_code != 0 ,
14271475 r. metadata ,
1476+ r. images ,
14281477 ) ,
14291478 Err ( e) => (
14301479 format ! ( "Tool execution error: {}" , e) ,
14311480 1 ,
14321481 true ,
14331482 None ,
1483+ Vec :: new ( ) ,
14341484 ) ,
14351485 }
14361486 } else {
@@ -1439,15 +1489,15 @@ impl AgentLoop {
14391489 tool_call. name,
14401490 response. reason. unwrap_or_else( || "No reason provided" . to_string( ) )
14411491 ) ;
1442- ( rejection_msg, 1 , true , None )
1492+ ( rejection_msg, 1 , true , None , Vec :: new ( ) )
14431493 }
14441494 }
14451495 Ok ( Err ( _) ) => {
14461496 let msg = format ! (
14471497 "Tool '{}' confirmation failed: confirmation channel closed" ,
14481498 tool_call. name
14491499 ) ;
1450- ( msg, 1 , true , None )
1500+ ( msg, 1 , true , None , Vec :: new ( ) )
14511501 }
14521502 Err ( _) => {
14531503 cm. check_timeouts ( ) . await ;
@@ -1458,7 +1508,7 @@ impl AgentLoop {
14581508 "Tool '{}' execution timed out waiting for confirmation ({}ms). Execution rejected." ,
14591509 tool_call. name, timeout_ms
14601510 ) ;
1461- ( msg, 1 , true , None )
1511+ ( msg, 1 , true , None , Vec :: new ( ) )
14621512 }
14631513 crate :: hitl:: TimeoutAction :: AutoApprove => {
14641514 let stream_ctx = self . streaming_tool_context (
@@ -1480,12 +1530,14 @@ impl AgentLoop {
14801530 r. exit_code ,
14811531 r. exit_code != 0 ,
14821532 r. metadata ,
1533+ r. images ,
14831534 ) ,
14841535 Err ( e) => (
14851536 format ! ( "Tool execution error: {}" , e) ,
14861537 1 ,
14871538 true ,
14881539 None ,
1540+ Vec :: new ( ) ,
14891541 ) ,
14901542 }
14911543 }
@@ -1503,7 +1555,7 @@ impl AgentLoop {
15031555 tool_name = tool_call. name. as_str( ) ,
15041556 "Tool requires confirmation but no HITL manager configured"
15051557 ) ;
1506- ( msg, 1 , true , None )
1558+ ( msg, 1 , true , None , Vec :: new ( ) )
15071559 }
15081560 }
15091561 } ;
@@ -1535,7 +1587,16 @@ impl AgentLoop {
15351587 }
15361588
15371589 // Add tool result to messages
1538- messages. push ( Message :: tool_result ( & tool_call. id , & output, is_error) ) ;
1590+ if images. is_empty ( ) {
1591+ messages. push ( Message :: tool_result ( & tool_call. id , & output, is_error) ) ;
1592+ } else {
1593+ messages. push ( Message :: tool_result_with_images (
1594+ & tool_call. id ,
1595+ & output,
1596+ & images,
1597+ is_error,
1598+ ) ) ;
1599+ }
15391600 }
15401601 }
15411602 }
0 commit comments