@@ -95,16 +95,62 @@ class _ToolExecutionInterrupted(Exception):
9595
9696ToolExecutorResultT = T .TypeVar ("ToolExecutorResultT" )
9797
98- USER_INTERRUPTION_MESSAGE = (
99- "[SYSTEM: User actively interrupted the response generation. "
100- "Partial output before interruption is preserved.]"
101- )
102-
10398
10499class ToolLoopAgentRunner (BaseAgentRunner [TContext ]):
105100 EMPTY_OUTPUT_RETRY_ATTEMPTS = 3
106101 EMPTY_OUTPUT_RETRY_WAIT_MIN_S = 1
107102 EMPTY_OUTPUT_RETRY_WAIT_MAX_S = 4
103+ USER_INTERRUPTION_MESSAGE = (
104+ "[SYSTEM: User actively interrupted the response generation. "
105+ "Partial output before interruption is preserved.]"
106+ )
107+ FOLLOW_UP_NOTICE_TEMPLATE = (
108+ "\n \n [SYSTEM NOTICE] User sent follow-up messages while tool execution "
109+ "was in progress. Prioritize these follow-up instructions in your next "
110+ "actions. In your very next action, briefly acknowledge to the user "
111+ "that their follow-up message(s) were received before continuing.\n "
112+ "{follow_up_lines}"
113+ )
114+ MAX_STEPS_REACHED_PROMPT = (
115+ "Maximum tool call limit reached. "
116+ "Stop calling tools, and based on the information you have gathered, "
117+ "summarize your task and findings, and reply to the user directly."
118+ )
119+ SKILLS_LIKE_REQUERY_INSTRUCTION_TEMPLATE = (
120+ "You have decided to call tool(s): {tool_names}. Now call the tool(s) "
121+ "with required arguments using the tool schema, and follow the existing "
122+ "tool-use rules."
123+ )
124+ SKILLS_LIKE_REQUERY_REPAIR_INSTRUCTION = (
125+ "This is the second-stage tool execution step. "
126+ "You must do exactly one of the following: "
127+ "1. Call one of the selected tools using the provided tool schema. "
128+ "2. If calling a tool is no longer possible or appropriate, reply to the user "
129+ "with a brief explanation of why. "
130+ "Do not return an empty response. "
131+ "Do not ignore the selected tools without explanation."
132+ )
133+ REPEATED_TOOL_NOTICE_L1_THRESHOLD = 3
134+ REPEATED_TOOL_NOTICE_L2_THRESHOLD = 4
135+ REPEATED_TOOL_NOTICE_L3_THRESHOLD = 5
136+ REPEATED_TOOL_NOTICE_L1_TEMPLATE = (
137+ "\n \n [SYSTEM NOTICE] By the way, you have executed the same tool "
138+ "`{tool_name}` {streak} times consecutively. Double-check whether another "
139+ "tool, different arguments, or a summary would move the task forward better."
140+ )
141+ REPEATED_TOOL_NOTICE_L2_TEMPLATE = (
142+ "\n \n [SYSTEM NOTICE] Important: you have executed the same tool "
143+ "`{tool_name}` {streak} times consecutively. Unless this repetition is "
144+ "clearly necessary, stop repeating the same action and either switch "
145+ "tools, refine parameters, or summarize what is still missing."
146+ )
147+ REPEATED_TOOL_NOTICE_L3_TEMPLATE = (
148+ "\n \n [SYSTEM NOTICE] Important: you have executed the same tool "
149+ "`{tool_name}` {streak} times consecutively. Repetition is now very "
150+ "high. Continue only if each call is clearly producing new information. "
151+ "Otherwise, change strategy, adjust arguments, or explain the limitation "
152+ "to the user."
153+ )
108154
109155 def _get_persona_custom_error_message (self ) -> str | None :
110156 """Read persona-level custom error message from event extras when available."""
@@ -209,6 +255,8 @@ async def reset(
209255 self ._abort_signal = asyncio .Event ()
210256 self ._pending_follow_ups : list [FollowUpTicket ] = []
211257 self ._follow_up_seq = 0
258+ self ._last_tool_name : str | None = None
259+ self ._same_tool_streak = 0
212260
213261 # These two are used for tool schema mode handling
214262 # We now have two modes:
@@ -413,12 +461,8 @@ def _consume_follow_up_notice(self) -> str:
413461 follow_up_lines = "\n " .join (
414462 f"{ idx } . { ticket .text } " for idx , ticket in enumerate (follow_ups , start = 1 )
415463 )
416- return (
417- "\n \n [SYSTEM NOTICE] User sent follow-up messages while tool execution "
418- "was in progress. Prioritize these follow-up instructions in your next "
419- "actions. In your very next action, briefly acknowledge to the user "
420- "that their follow-up message(s) were received before continuing.\n "
421- f"{ follow_up_lines } "
464+ return self .FOLLOW_UP_NOTICE_TEMPLATE .format (
465+ follow_up_lines = follow_up_lines ,
422466 )
423467
424468 def _merge_follow_up_notice (self , content : str ) -> str :
@@ -427,6 +471,35 @@ def _merge_follow_up_notice(self, content: str) -> str:
427471 return content
428472 return f"{ content } { notice } "
429473
474+ def _track_tool_call_streak (self , tool_name : str ) -> int :
475+ if tool_name == self ._last_tool_name :
476+ self ._same_tool_streak += 1
477+ else :
478+ self ._last_tool_name = tool_name
479+ self ._same_tool_streak = 1
480+ return self ._same_tool_streak
481+
482+ def _build_repeated_tool_call_guidance (self , tool_name : str , streak : int ) -> str :
483+ if streak < self .REPEATED_TOOL_NOTICE_L1_THRESHOLD :
484+ return ""
485+
486+ if streak >= self .REPEATED_TOOL_NOTICE_L3_THRESHOLD :
487+ return self .REPEATED_TOOL_NOTICE_L3_TEMPLATE .format (
488+ tool_name = tool_name ,
489+ streak = streak ,
490+ )
491+
492+ if streak >= self .REPEATED_TOOL_NOTICE_L2_THRESHOLD :
493+ return self .REPEATED_TOOL_NOTICE_L2_TEMPLATE .format (
494+ tool_name = tool_name ,
495+ streak = streak ,
496+ )
497+
498+ return self .REPEATED_TOOL_NOTICE_L1_TEMPLATE .format (
499+ tool_name = tool_name ,
500+ streak = streak ,
501+ )
502+
430503 @override
431504 async def step (self ):
432505 """Process a single step of the agent.
@@ -483,7 +556,7 @@ async def step(self):
483556 if self ._is_stop_requested ():
484557 llm_resp_result = LLMResponse (
485558 role = "assistant" ,
486- completion_text = USER_INTERRUPTION_MESSAGE ,
559+ completion_text = self . USER_INTERRUPTION_MESSAGE ,
487560 reasoning_content = llm_response .reasoning_content ,
488561 reasoning_signature = llm_response .reasoning_signature ,
489562 )
@@ -681,7 +754,7 @@ async def step_until_done(
681754 self .run_context .messages .append (
682755 Message (
683756 role = "user" ,
684- content = "工具调用次数已达到上限,请停止使用工具,并根据已经收集到的信息,对你的任务和发现进行总结,然后直接回复用户。" ,
757+ content = self . MAX_STEPS_REACHED_PROMPT ,
685758 )
686759 )
687760 # 再执行最后一步
@@ -712,6 +785,7 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None:
712785 llm_response .tools_call_args ,
713786 llm_response .tools_call_ids ,
714787 ):
788+ tool_call_streak = self ._track_tool_call_streak (func_tool_name )
715789 yield _HandleFunctionToolsResult .from_message_chain (
716790 MessageChain (
717791 type = "tool_call" ,
@@ -861,7 +935,10 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None:
861935 if result_parts :
862936 _append_tool_call_result (
863937 func_tool_id ,
864- "\n \n " .join (result_parts ),
938+ "\n \n " .join (result_parts )
939+ + self ._build_repeated_tool_call_guidance (
940+ func_tool_name , tool_call_streak
941+ ),
865942 )
866943
867944 elif resp is None :
@@ -875,7 +952,10 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None:
875952 self .stats .end_time = time .time ()
876953 _append_tool_call_result (
877954 func_tool_id ,
878- "The tool has no return value, or has sent the result directly to the user." ,
955+ "The tool has no return value, or has sent the result directly to the user."
956+ + self ._build_repeated_tool_call_guidance (
957+ func_tool_name , tool_call_streak
958+ ),
879959 )
880960 else :
881961 # 不应该出现其他类型
@@ -884,7 +964,10 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None:
884964 )
885965 _append_tool_call_result (
886966 func_tool_id ,
887- "*The tool has returned an unsupported type. Please tell the user to check the definition and implementation of this tool.*" ,
967+ "*The tool has returned an unsupported type. Please tell the user to check the definition and implementation of this tool.*"
968+ + self ._build_repeated_tool_call_guidance (
969+ func_tool_name , tool_call_streak
970+ ),
888971 )
889972
890973 try :
@@ -902,7 +985,10 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None:
902985 logger .warning (traceback .format_exc ())
903986 _append_tool_call_result (
904987 func_tool_id ,
905- f"error: { e !s} " ,
988+ f"error: { e !s} "
989+ + self ._build_repeated_tool_call_guidance (
990+ func_tool_name , tool_call_streak
991+ ),
906992 )
907993
908994 # yield the last tool call result
@@ -942,11 +1028,8 @@ def _build_tool_requery_context(
9421028 contexts .append (msg .model_dump ()) # type: ignore[call-arg]
9431029 elif isinstance (msg , dict ):
9441030 contexts .append (copy .deepcopy (msg ))
945- instruction = (
946- "You have decided to call tool(s): "
947- + ", " .join (tool_names )
948- + ". Now call the tool(s) with required arguments using the tool schema, "
949- "and follow the existing tool-use rules."
1031+ instruction = self .SKILLS_LIKE_REQUERY_INSTRUCTION_TEMPLATE .format (
1032+ tool_names = ", " .join (tool_names )
9501033 )
9511034 if extra_instruction :
9521035 instruction = f"{ instruction } \n { extra_instruction } "
@@ -1017,14 +1100,7 @@ async def _resolve_tool_exec(
10171100 )
10181101 repair_contexts = self ._build_tool_requery_context (
10191102 tool_names ,
1020- extra_instruction = (
1021- "This is the second-stage tool execution step. "
1022- "You must do exactly one of the following: "
1023- "1. Call one of the selected tools using the provided tool schema. "
1024- "2. If calling a tool is no longer possible or appropriate, reply to the user with a brief explanation of why. "
1025- "Do not return an empty response. "
1026- "Do not ignore the selected tools without explanation."
1027- ),
1103+ extra_instruction = self .SKILLS_LIKE_REQUERY_REPAIR_INSTRUCTION ,
10281104 )
10291105 repair_resp = await self .provider .text_chat (
10301106 contexts = repair_contexts ,
@@ -1066,7 +1142,7 @@ async def _finalize_aborted_step(
10661142 if llm_resp .role != "assistant" :
10671143 llm_resp = LLMResponse (
10681144 role = "assistant" ,
1069- completion_text = USER_INTERRUPTION_MESSAGE ,
1145+ completion_text = self . USER_INTERRUPTION_MESSAGE ,
10701146 )
10711147 self .final_llm_resp = llm_resp
10721148 self ._aborted = True
0 commit comments