@@ -32,6 +32,7 @@ use super::hooks::{
3232} ;
3333use super :: message:: {
3434 AssistantMessage ,
35+ AssistantToolUse ,
3536 ToolUseResult ,
3637 ToolUseResultBlock ,
3738 UserMessage ,
@@ -344,53 +345,122 @@ impl ConversationState {
344345 tool_uses. iter ( ) . map ( |t| t. id . as_str ( ) ) ,
345346 ) ;
346347 }
348+ }
347349
348- // Here we also need to make sure that the tool result corresponds to one of the tools
349- // in the list. Otherwise we will see validation error from the backend. There are three
350- // such circumstances where intervention would be needed:
351- // 1. The model had decided to call a tool with its partial name AND there is only one such tool, in
352- // which case we would automatically resolve this tool call to its correct name. This will NOT
353- // result in an error in its tool result. The intervention here is to substitute the partial name
354- // with its full name.
355- // 2. The model had decided to call a tool with its partial name AND there are multiple tools it
356- // could be referring to, in which case we WILL return an error in the tool result. The
357- // intervention here is to substitute the ambiguous, partial name with a dummy.
358- // 3. The model had decided to call a tool that does not exist. The intervention here is to
359- // substitute the non-existent tool name with a dummy.
360- let tool_use_results = user_msg. tool_use_results ( ) ;
361- if let Some ( tool_use_results) = tool_use_results {
362- // Note that we need to use the keys in tool manager's tn_map as the keys are the
363- // actual tool names as exposed to the model and the backend. If we use the actual
364- // names as they are recognized by their respective servers, we risk concluding
365- // with false positives.
366- let tool_name_list = self . tool_manager . tn_map . keys ( ) . map ( String :: as_str) . collect :: < Vec < _ > > ( ) ;
367- for result in tool_use_results {
368- let tool_use_id = result. tool_use_id . as_str ( ) ;
369- let corresponding_tool_use = tool_uses. iter_mut ( ) . find ( |tool_use| tool_use_id == tool_use. id ) ;
370- if let Some ( tool_use) = corresponding_tool_use {
371- if tool_name_list. contains ( & tool_use. name . as_str ( ) ) {
372- // If this tool matches of the tools in our list, this is not our
373- // concern, error or not.
374- continue ;
375- }
376- if let ToolResultStatus :: Error = result. status {
377- // case 2 and 3
378- tool_use. name = DUMMY_TOOL_NAME . to_string ( ) ;
379- tool_use. args = serde_json:: json!( { } ) ;
380- } else {
381- // case 1
382- let full_name = tool_name_list. iter ( ) . find ( |name| name. ends_with ( & tool_use. name ) ) ;
383- // We should be able to find a match but if not we'll just treat it as
384- // a dummy and move on
385- if let Some ( full_name) = full_name {
386- tool_use. name = ( * full_name) . to_string ( ) ;
387- } else {
388- tool_use. name = DUMMY_TOOL_NAME . to_string ( ) ;
389- tool_use. args = serde_json:: json!( { } ) ;
390- }
391- }
350+ self . enforce_tool_use_history_invariants ( true ) ;
351+ }
352+
353+ /// Here we also need to make sure that the tool result corresponds to one of the tools
354+ /// in the list. Otherwise we will see validation error from the backend. There are three
355+ /// such circumstances where intervention would be needed:
356+ /// 1. The model had decided to call a tool with its partial name AND there is only one such
357+ /// tool, in which case we would automatically resolve this tool call to its correct name.
358+ /// This will NOT result in an error in its tool result. The intervention here is to
359+ /// substitute the partial name with its full name.
360+ /// 2. The model had decided to call a tool with its partial name AND there are multiple tools
361+ /// it could be referring to, in which case we WILL return an error in the tool result. The
362+ /// intervention here is to substitute the ambiguous, partial name with a dummy.
363+ /// 3. The model had decided to call a tool that does not exist. The intervention here is to
364+ /// substitute the non-existent tool name with a dummy.
365+ pub fn enforce_tool_use_history_invariants ( & mut self , last_only : bool ) {
366+ let tool_name_list = self . tool_manager . tn_map . keys ( ) . map ( String :: as_str) . collect :: < Vec < _ > > ( ) ;
367+ // We need to first determine what the range of interest is. There are two places where we
368+ // would call this function:
369+ // 1. When there are changes to the list of available tools, in which case we comb through the
370+ // entire conversation
371+ // 2. When we send a message, in which case we only examine the most recent entry
372+ let ( tool_use_results, mut tool_uses) = if last_only {
373+ if let ( Some ( ( _, AssistantMessage :: ToolUse { ref mut tool_uses, .. } ) ) , Some ( user_msg) ) = (
374+ self . history
375+ . range_mut ( self . valid_history_range . 0 ..self . valid_history_range . 1 )
376+ . last ( ) ,
377+ & mut self . next_message ,
378+ ) {
379+ let tool_use_results = user_msg
380+ . tool_use_results ( )
381+ . map_or ( Vec :: new ( ) , |results| results. iter ( ) . collect :: < Vec < _ > > ( ) ) ;
382+ let tool_uses = tool_uses. iter_mut ( ) . collect :: < Vec < _ > > ( ) ;
383+ ( tool_use_results, tool_uses)
384+ } else {
385+ ( Vec :: new ( ) , Vec :: new ( ) )
386+ }
387+ } else {
388+ let tool_use_results = self . next_message . as_ref ( ) . map_or ( Vec :: new ( ) , |user_msg| {
389+ user_msg
390+ . tool_use_results ( )
391+ . map_or ( Vec :: new ( ) , |results| results. iter ( ) . collect :: < Vec < _ > > ( ) )
392+ } ) ;
393+ self . history
394+ . iter_mut ( )
395+ . filter_map ( |( user_msg, asst_msg) | {
396+ if let ( Some ( tool_use_results) , AssistantMessage :: ToolUse { ref mut tool_uses, .. } ) =
397+ ( user_msg. tool_use_results ( ) , asst_msg)
398+ {
399+ Some ( ( tool_use_results, tool_uses) )
400+ } else {
401+ None
392402 }
403+ } )
404+ . fold (
405+ ( tool_use_results, Vec :: < & mut AssistantToolUse > :: new ( ) ) ,
406+ |( mut tool_use_results, mut tool_uses) , ( results, uses) | {
407+ let mut results = results. iter ( ) . collect :: < Vec < _ > > ( ) ;
408+ let mut uses = uses. iter_mut ( ) . collect :: < Vec < _ > > ( ) ;
409+ tool_use_results. append ( & mut results) ;
410+ tool_uses. append ( & mut uses) ;
411+ ( tool_use_results, tool_uses)
412+ } ,
413+ )
414+ } ;
415+
416+ // Replace tool uses associated with tools that does not exist / no longer exists with
417+ // dummy (i.e. put them to sleep / dormant)
418+ for result in tool_use_results {
419+ let tool_use_id = result. tool_use_id . as_str ( ) ;
420+ let corresponding_tool_use = tool_uses. iter_mut ( ) . find ( |tool_use| tool_use_id == tool_use. id ) ;
421+ if let Some ( tool_use) = corresponding_tool_use {
422+ if tool_name_list. contains ( & tool_use. name . as_str ( ) ) {
423+ // If this tool matches of the tools in our list, this is not our
424+ // concern, error or not.
425+ continue ;
393426 }
427+ if let ToolResultStatus :: Error = result. status {
428+ // case 2 and 3
429+ tool_use. name = DUMMY_TOOL_NAME . to_string ( ) ;
430+ tool_use. args = serde_json:: json!( { } ) ;
431+ } else {
432+ // case 1
433+ let full_name = tool_name_list. iter ( ) . find ( |name| name. ends_with ( & tool_use. name ) ) ;
434+ // We should be able to find a match but if not we'll just treat it as
435+ // a dummy and move on
436+ if let Some ( full_name) = full_name {
437+ tool_use. name = ( * full_name) . to_string ( ) ;
438+ } else {
439+ tool_use. name = DUMMY_TOOL_NAME . to_string ( ) ;
440+ tool_use. args = serde_json:: json!( { } ) ;
441+ }
442+ }
443+ }
444+ }
445+
446+ // Revive tools that were previously dormant if they now corresponds to one of the tools in
447+ // our list of available tools. Note that this check only works because tn_map does NOT
448+ // contain names of native tools.
449+ for tool_use in tool_uses {
450+ if tool_use. name == DUMMY_TOOL_NAME
451+ && tool_use
452+ . orig_name
453+ . as_ref ( )
454+ . is_some_and ( |name| tool_name_list. contains ( & ( * name) . as_str ( ) ) )
455+ {
456+ tool_use. name = tool_use
457+ . orig_name
458+ . as_ref ( )
459+ . map_or ( DUMMY_TOOL_NAME . to_string ( ) , |name| name. clone ( ) ) ;
460+ tool_use. args = tool_use
461+ . orig_args
462+ . as_ref ( )
463+ . map_or ( serde_json:: json!( { } ) , |args| args. clone ( ) ) ;
394464 }
395465 }
396466 }
@@ -419,7 +489,6 @@ impl ConversationState {
419489 /// - `run_hooks` - whether hooks should be executed and included as context
420490 pub async fn as_sendable_conversation_state ( & mut self , run_hooks : bool ) -> FigConversationState {
421491 debug_assert ! ( self . next_message. is_some( ) ) ;
422- self . update_state ( ) . await ;
423492 self . enforce_conversation_invariants ( ) ;
424493 self . history . drain ( self . valid_history_range . 1 ..) ;
425494 self . history . drain ( ..self . valid_history_range . 0 ) ;
@@ -451,6 +520,7 @@ impl ConversationState {
451520 return ;
452521 }
453522 self . tool_manager . update ( ) . await ;
523+ // TODO: make this more targeted so we don't have to clone the entire list of tools
454524 self . tools = self
455525 . tool_manager
456526 . schema
@@ -467,6 +537,10 @@ impl ConversationState {
467537 acc
468538 } ) ;
469539 self . tool_manager . has_new_stuff . store ( false , Ordering :: Release ) ;
540+ // We call this in [Self::enforce_conversation_invariants] as well. But we need to call it
541+ // here as well because when it's being called in [Self::enforce_conversation_invariants]
542+ // it is only checking the last entry.
543+ self . enforce_tool_use_history_invariants ( false ) ;
470544 }
471545
472546 /// Returns a conversation state representation which reflects the exact conversation to send
@@ -1066,6 +1140,7 @@ mod tests {
10661140 id: "tool_id" . to_string( ) ,
10671141 name: "tool name" . to_string( ) ,
10681142 args: serde_json:: Value :: Null ,
1143+ ..Default :: default ( )
10691144 } ] ) ,
10701145 & mut database,
10711146 ) ;
@@ -1096,6 +1171,7 @@ mod tests {
10961171 id: "tool_id" . to_string( ) ,
10971172 name: "tool name" . to_string( ) ,
10981173 args: serde_json:: Value :: Null ,
1174+ ..Default :: default ( )
10991175 } ] ) ,
11001176 & mut database,
11011177 ) ;
0 commit comments