@@ -100,6 +100,32 @@ def _get_persona_custom_error_message(self) -> str | None:
100100 event = getattr (self .run_context .context , "event" , None )
101101 return extract_persona_custom_error_message_from_event (event )
102102
103+ async def _complete_with_assistant_response (self , llm_resp : LLMResponse ) -> None :
104+ """Finalize the current step as a plain assistant response with no tool calls."""
105+ self .final_llm_resp = llm_resp
106+ self ._transition_state (AgentState .DONE )
107+ self .stats .end_time = time .time ()
108+
109+ parts = []
110+ if llm_resp .reasoning_content or llm_resp .reasoning_signature :
111+ parts .append (
112+ ThinkPart (
113+ think = llm_resp .reasoning_content ,
114+ encrypted = llm_resp .reasoning_signature ,
115+ )
116+ )
117+ if llm_resp .completion_text :
118+ parts .append (TextPart (text = llm_resp .completion_text ))
119+ if len (parts ) == 0 :
120+ logger .warning ("LLM returned empty assistant message with no tool calls." )
121+ self .run_context .messages .append (Message (role = "assistant" , content = parts ))
122+
123+ try :
124+ await self .agent_hooks .on_agent_done (self .run_context , llm_resp )
125+ except Exception as e :
126+ logger .error (f"Error in on_agent_done hook: { e } " , exc_info = True )
127+ self ._resolve_unconsumed_follow_ups ()
128+
103129 @override
104130 async def reset (
105131 self ,
@@ -463,34 +489,7 @@ async def step(self):
463489 return
464490
465491 if not llm_resp .tools_call_name :
466- # 如果没有工具调用,转换到完成状态
467- self .final_llm_resp = llm_resp
468- self ._transition_state (AgentState .DONE )
469- self .stats .end_time = time .time ()
470-
471- # record the final assistant message
472- parts = []
473- if llm_resp .reasoning_content or llm_resp .reasoning_signature :
474- parts .append (
475- ThinkPart (
476- think = llm_resp .reasoning_content ,
477- encrypted = llm_resp .reasoning_signature ,
478- )
479- )
480- if llm_resp .completion_text :
481- parts .append (TextPart (text = llm_resp .completion_text ))
482- if len (parts ) == 0 :
483- logger .warning (
484- "LLM returned empty assistant message with no tool calls."
485- )
486- self .run_context .messages .append (Message (role = "assistant" , content = parts ))
487-
488- # call the on_agent_done hook
489- try :
490- await self .agent_hooks .on_agent_done (self .run_context , llm_resp )
491- except Exception as e :
492- logger .error (f"Error in on_agent_done hook: { e } " , exc_info = True )
493- self ._resolve_unconsumed_follow_ups ()
492+ await self ._complete_with_assistant_response (llm_resp )
494493
495494 # 返回 LLM 结果
496495 if llm_resp .result_chain :
@@ -510,6 +509,24 @@ async def step(self):
510509 if llm_resp .tools_call_name :
511510 if self .tool_schema_mode == "skills_like" :
512511 llm_resp , _ = await self ._resolve_tool_exec (llm_resp )
512+ if not llm_resp .tools_call_name :
513+ logger .warning (
514+ "skills_like tool re-query returned no tool calls; fallback to assistant response."
515+ )
516+ if llm_resp .result_chain :
517+ yield AgentResponse (
518+ type = "llm_result" ,
519+ data = AgentResponseData (chain = llm_resp .result_chain ),
520+ )
521+ elif llm_resp .completion_text :
522+ yield AgentResponse (
523+ type = "llm_result" ,
524+ data = AgentResponseData (
525+ chain = MessageChain ().message (llm_resp .completion_text ),
526+ ),
527+ )
528+ await self ._complete_with_assistant_response (llm_resp )
529+ return
513530
514531 tool_call_result_blocks = []
515532 cached_images = [] # Collect cached images for LLM visibility
@@ -873,7 +890,9 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None:
873890 )
874891
875892 def _build_tool_requery_context (
876- self , tool_names : list [str ]
893+ self ,
894+ tool_names : list [str ],
895+ extra_instruction : str | None = None ,
877896 ) -> list [dict [str , T .Any ]]:
878897 """Build contexts for re-querying LLM with param-only tool schemas."""
879898 contexts : list [dict [str , T .Any ]] = []
@@ -888,13 +907,20 @@ def _build_tool_requery_context(
888907 + ". Now call the tool(s) with required arguments using the tool schema, "
889908 "and follow the existing tool-use rules."
890909 )
910+ if extra_instruction :
911+ instruction = f"{ instruction } \n { extra_instruction } "
891912 if contexts and contexts [0 ].get ("role" ) == "system" :
892913 content = contexts [0 ].get ("content" ) or ""
893914 contexts [0 ]["content" ] = f"{ content } \n { instruction } "
894915 else :
895916 contexts .insert (0 , {"role" : "system" , "content" : instruction })
896917 return contexts
897918
919+ @staticmethod
920+ def _has_meaningful_assistant_reply (llm_resp : LLMResponse ) -> bool :
921+ text = (llm_resp .completion_text or "" ).strip ()
922+ return bool (text )
923+
898924 def _build_tool_subset (self , tool_set : ToolSet , tool_names : list [str ]) -> ToolSet :
899925 """Build a subset of tools from the given tool set based on tool names."""
900926 subset = ToolSet ()
@@ -932,11 +958,45 @@ async def _resolve_tool_exec(
932958 model = self .req .model ,
933959 session_id = self .req .session_id ,
934960 extra_user_content_parts = self .req .extra_user_content_parts ,
961+ tool_choice = "required" ,
935962 abort_signal = self ._abort_signal ,
936963 )
937964 if requery_resp :
938965 llm_resp = requery_resp
939966
967+ # If the re-query still returns no tool calls, and also does not have a meaningful assistant reply,
968+ # we consider it as a failure of the LLM to follow the tool-use instruction,
969+ # and we will retry once with a stronger instruction that explicitly requires the LLM to either call the tool or give an explanation.
970+ if (
971+ not llm_resp .tools_call_name
972+ and not self ._has_meaningful_assistant_reply (llm_resp )
973+ ):
974+ logger .warning (
975+ "skills_like tool re-query returned no tool calls and no explanation; retrying with stronger instruction."
976+ )
977+ repair_contexts = self ._build_tool_requery_context (
978+ tool_names ,
979+ extra_instruction = (
980+ "This is the second-stage tool execution step. "
981+ "You must do exactly one of the following: "
982+ "1. Call one of the selected tools using the provided tool schema. "
983+ "2. If calling a tool is no longer possible or appropriate, reply to the user with a brief explanation of why. "
984+ "Do not return an empty response. "
985+ "Do not ignore the selected tools without explanation."
986+ ),
987+ )
988+ repair_resp = await self .provider .text_chat (
989+ contexts = repair_contexts ,
990+ func_tool = param_subset ,
991+ model = self .req .model ,
992+ session_id = self .req .session_id ,
993+ extra_user_content_parts = self .req .extra_user_content_parts ,
994+ tool_choice = "required" ,
995+ abort_signal = self ._abort_signal ,
996+ )
997+ if repair_resp :
998+ llm_resp = repair_resp
999+
9401000 return llm_resp , subset
9411001
9421002 def done (self ) -> bool :
0 commit comments