@@ -158,6 +158,7 @@ async def reset(
158158 self ._aborted = False
159159 self ._pending_follow_ups : list [FollowUpTicket ] = []
160160 self ._follow_up_seq = 0
161+ self ._unknown_tool_consecutive_count = 0 # Track consecutive unknown tool calls
161162
162163 # These two are used for tool schema mode handling
163164 # We now have two modes:
@@ -246,6 +247,24 @@ async def _iter_llm_responses_with_fallback(
246247 yield resp
247248 continue
248249
250+ if resp .role == "err_retry" :
251+ # Empty/unparseable response from model, retry same provider once
252+ logger .warning (
253+ "Chat Model %s returned empty/unparseable completion, retrying once..." ,
254+ candidate_id ,
255+ )
256+ try :
257+ async for retry_resp in self ._iter_llm_responses (include_model = idx == 0 ):
258+ if retry_resp .is_chunk :
259+ yield retry_resp
260+ continue
261+ yield retry_resp
262+ return
263+ except Exception as retry_exc :
264+ logger .warning ("Retry also failed: %s" , retry_exc )
265+ last_exception = retry_exc
266+ break
267+
249268 if (
250269 resp .role == "err"
251270 and not has_stream_output
@@ -265,6 +284,53 @@ async def _iter_llm_responses_with_fallback(
265284 return
266285 except Exception as exc : # noqa: BLE001
267286 last_exception = exc
287+ # Auto-compress context when model_max_prompt_tokens_exceeded
288+ _exc_str = str (exc ).lower ()
289+ if (
290+ "model_max_prompt_tokens_exceeded" in _exc_str
291+ or "prompt token count" in _exc_str
292+ or "tokens_exceeded" in _exc_str
293+ or "context_length_exceeded" in _exc_str
294+ or ("token" in _exc_str and "exceed" in _exc_str )
295+ ):
296+ logger .warning (
297+ "Chat Model %s: token limit exceeded, forcing context compression and retrying..." ,
298+ candidate_id ,
299+ )
300+ try :
301+ _before = len (self .run_context .messages )
302+ # Force halving truncation regardless of threshold
303+ from astrbot .core .agent .context .truncator import ContextTruncator
304+ _truncator = ContextTruncator ()
305+ self .run_context .messages = _truncator .truncate_by_halving (
306+ self .run_context .messages
307+ )
308+ _after = len (self .run_context .messages )
309+ logger .info (
310+ "Forced context truncation: %d -> %d messages, retrying same provider." ,
311+ _before , _after ,
312+ )
313+ # Retry same candidate once after compression
314+ try :
315+ async for resp in self ._iter_llm_responses (include_model = idx == 0 ):
316+ if resp .is_chunk :
317+ has_stream_output = True
318+ yield resp
319+ continue
320+ yield resp
321+ return
322+ if has_stream_output :
323+ return
324+ except Exception as retry_exc :
325+ last_exception = retry_exc
326+ logger .warning (
327+ "Chat Model %s retry after compression also failed: %s" ,
328+ candidate_id ,
329+ retry_exc ,
330+ )
331+ except Exception as compress_exc :
332+ logger .error ("Failed to compress context: %s" , compress_exc )
333+ continue
268334 logger .warning (
269335 "Chat Model %s request error: %s" ,
270336 candidate_id ,
@@ -332,10 +398,11 @@ def _consume_follow_up_notice(self) -> str:
332398 f"{ idx } . { ticket .text } " for idx , ticket in enumerate (follow_ups , start = 1 )
333399 )
334400 return (
335- "\n \n [SYSTEM NOTICE] User sent follow-up messages while tool execution "
336- "was in progress. Prioritize these follow-up instructions in your next "
337- "actions. In your very next action, briefly acknowledge to the user "
338- "that their follow-up message(s) were received before continuing.\n "
401+ "\n \n [FOLLOW-UP] The user sent additional message(s) while you were working. "
402+ "Treat these as supplementary instructions for the current task — DO NOT stop "
403+ "or restart the current operation. Instead, seamlessly incorporate them into "
404+ "your ongoing work. Continue the task flow without interrupting it. "
405+ "Do NOT acknowledge receipt explicitly; just act on them naturally.\n "
339406 f"{ follow_up_lines } "
340407 )
341408
@@ -532,6 +599,25 @@ async def step(self):
532599 if self .tool_schema_mode == "skills_like" :
533600 llm_resp , _ = await self ._resolve_tool_exec (llm_resp )
534601
602+ # Detect sentinel set by openai_source when LLM called an unknown tool.
603+ # We still run _handle_function_tools so tool-result error messages are
604+ # injected into the context, but afterwards we force the agent to DONE
605+ # only if this happens consecutively (to avoid false positives from param errors).
606+ _is_unknown_tool_call = (
607+ llm_resp .completion_text == "__UNKNOWN_TOOL_STOP__"
608+ )
609+ if _is_unknown_tool_call :
610+ self ._unknown_tool_consecutive_count += 1
611+ llm_resp .completion_text = "" # clear sentinel before passing to handler
612+ else :
613+ self ._unknown_tool_consecutive_count = 0
614+ # Only force DONE after 2+ consecutive unknown tool calls
615+ _UNKNOWN_TOOL_MAX_RETRIES = 2
616+ _force_done_after_tool_handling = (
617+ _is_unknown_tool_call
618+ and self ._unknown_tool_consecutive_count >= _UNKNOWN_TOOL_MAX_RETRIES
619+ )
620+
535621 tool_call_result_blocks = []
536622 cached_images = [] # Collect cached images for LLM visibility
537623 async for result in self ._handle_function_tools (self .req , llm_resp ):
@@ -618,6 +704,23 @@ async def step(self):
618704
619705 self .req .append_tool_calls_result (tool_calls_result )
620706
707+ # If flagged as unknown-tool, force DONE now so we don't loop back to LLM.
708+ if _force_done_after_tool_handling :
709+ logger .warning (
710+ f"Unknown tool call detected { self ._unknown_tool_consecutive_count } consecutive time(s); "
711+ "forcing agent DONE to prevent infinite retry loop."
712+ )
713+ self .final_llm_resp = llm_resp
714+ self ._transition_state (AgentState .DONE )
715+ self .stats .end_time = time .time ()
716+ # Preserve context: on_agent_done hook should still be called
717+ try :
718+ await self .agent_hooks .on_agent_done (self .run_context , llm_resp )
719+ except Exception as e :
720+ logger .error (f"Error in on_agent_done hook: { e } " , exc_info = True )
721+ self ._resolve_unconsumed_follow_ups ()
722+ return
723+
621724 async def step_until_done (
622725 self , max_step : int
623726 ) -> T .AsyncGenerator [AgentResponse , None ]:
@@ -706,7 +809,8 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None:
706809 logger .warning (f"未找到指定的工具: { func_tool_name } ,将跳过。" )
707810 _append_tool_call_result (
708811 func_tool_id ,
709- f"error: Tool { func_tool_name } not found." ,
812+ f"[SYSTEM ERROR] Tool '{ func_tool_name } ' is not available or does not exist. "
813+ f"Do NOT retry calling this tool. Please stop using tools and respond directly to the user based on information already gathered." ,
710814 )
711815 continue
712816
@@ -736,7 +840,36 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None:
736840 )
737841 else :
738842 # 如果没有 handler(如 MCP 工具),使用所有参数
739- valid_params = func_tool_args
843+ # 但如果工具有 call() 方法(is_override_call),尝试从 call() 签名过滤参数
844+ import inspect as _inspect
845+ if hasattr (func_tool , 'call' ):
846+ try :
847+ _sig = _inspect .signature (func_tool .call )
848+ _params = _sig .parameters
849+ # Check if the method accepts **kwargs (VAR_KEYWORD)
850+ _has_var_keyword = any (
851+ p .kind == _inspect .Parameter .VAR_KEYWORD
852+ for p in _params .values ()
853+ )
854+ if _has_var_keyword :
855+ # **kwargs accepts anything, pass all args through
856+ valid_params = func_tool_args
857+ else :
858+ # func_tool.call is a bound method: signature is (context, arg1, arg2, ...)
859+ # skip 'context' (index 0), real args start at index 1
860+ _param_names = list (_params .keys ())
861+ _expected = set (_param_names [1 :]) if len (_param_names ) > 1 else set ()
862+ if _expected :
863+ valid_params = {k : v for k , v in func_tool_args .items () if k in _expected }
864+ _ignored = set (func_tool_args .keys ()) - set (valid_params .keys ())
865+ if _ignored :
866+ logger .warning (f"工具 { func_tool_name } (call方式) 忽略非期望参数: { _ignored } " )
867+ else :
868+ valid_params = func_tool_args
869+ except Exception :
870+ valid_params = func_tool_args
871+ else :
872+ valid_params = func_tool_args
740873
741874 try :
742875 await self .agent_hooks .on_tool_start (
0 commit comments