Split unusable response loop control

Handle empty model responses and repeated response content in separate message-loop result extensions. Move the repeat retry log text into a framework prompt. Cover empty-response logging and repeated content with reasoning.

linkliti committed Aug 24, 2026 at 22:07 UTC dbbdd26cc54a1e433f6290ffe8420b77ea73d608
4 files changed +66 -16
extensions/python/message_loop_result/_20_empty_response.py renamed
+7 -14
@@ -1,4 +1,4 @@
1 -"""Handle completed model turns that should retry instead of dispatching tools."""
1 +"""Retry completed model turns with neither response nor reasoning."""
2
3 from __future__ import annotations
4
@@ -8,7 +8,7 @@ from helpers.extension import Extension
8 from helpers.print_style import PrintStyle
9
10
11 -class LoopControl(Extension):
11 +class EmptyResponse(Extension):
12 def execute(self, result_data: dict[str, Any] | None = None, **kwargs: Any) -> None:
13 if not self.agent or not isinstance(result_data, dict):
14 return
@@ -18,16 +18,10 @@ class LoopControl(Extension):
18 reasoning = getattr(llm_result, "reasoning", "")
19 if not isinstance(response, str) or not isinstance(reasoning, str):
20 return
21 -
22 - if not response.strip():
23 - if reasoning.strip():
24 - return
25 - warning = self.agent.read_prompt("fw.msg_empty_response.md")
26 - elif response != self.agent.loop_data.last_response:
21 + if response.strip() or reasoning.strip():
22 return
28 - else:
29 - warning = self.agent.read_prompt("fw.msg_repeat.md")
23
24 + warning = self.agent.read_prompt("fw.msg_empty_response.md")
25 log_item = self.agent.loop_data.params_temporary.get("log_item_generating")
26 assistant_message = self.agent.hist_add_ai_response(
27 response,
@@ -37,10 +31,9 @@ class LoopControl(Extension):
31 self.agent._remember_llm_result_state(llm_result, assistant_message)
32 warning_message = self.agent.hist_add_warning(message=warning)
33 PrintStyle(font_color="orange", padding=True).print(warning)
40 - log_content = warning
41 - if response:
42 - log_content = f"{self.agent.agent_name}: Repeated response detected. Retrying."
34 self.agent.context.log.log(
44 - type="warning", content=log_content, id=warning_message.id
35 + type="warning",
36 + content=f"{self.agent.agent_name}: {warning}",
37 + id=warning_message.id,
38 )
39 result_data["skip_default_processing"] = True
extensions/python/message_loop_result/_30_repeat_response.py new
+38
@@ -0,0 +1,38 @@
1 +"""Retry completed model turns that repeat the prior response."""
2 +
3 +from __future__ import annotations
4 +
5 +from typing import Any
6 +
7 +from helpers.extension import Extension
8 +from helpers.print_style import PrintStyle
9 +
10 +
11 +class RepeatResponse(Extension):
12 + def execute(self, result_data: dict[str, Any] | None = None, **kwargs: Any) -> None:
13 + if not self.agent or not isinstance(result_data, dict):
14 + return
15 +
16 + llm_result = result_data.get("llm_result")
17 + response = getattr(llm_result, "response", "")
18 + if not isinstance(response, str):
19 + return
20 + if not response.strip() or response != self.agent.loop_data.last_response:
21 + return
22 +
23 + warning = self.agent.read_prompt("fw.msg_repeat.md")
24 + log_item = self.agent.loop_data.params_temporary.get("log_item_generating")
25 + assistant_message = self.agent.hist_add_ai_response(
26 + response,
27 + id=log_item.id if log_item else "",
28 + llm_result=llm_result,
29 + )
30 + self.agent._remember_llm_result_state(llm_result, assistant_message)
31 + warning_message = self.agent.hist_add_warning(message=warning)
32 + PrintStyle(font_color="orange", padding=True).print(warning)
33 + self.agent.context.log.log(
34 + type="warning",
35 + content=f"{self.agent.agent_name}: {self.agent.read_prompt('fw.msg_repeat_response.md')}",
36 + id=warning_message.id,
37 + )
38 + result_data["skip_default_processing"] = True
prompts/fw.msg_repeat_response.md new
+1
@@ -0,0 +1 @@
1 +Repeated response detected. Retrying.
tests/test_message_loop_result.py
+20 -2
@@ -1,6 +1,7 @@
1 from types import SimpleNamespace
2
3 -from extensions.python.message_loop_result._20_loop_control import LoopControl
3 +from extensions.python.message_loop_result._20_empty_response import EmptyResponse
4 +from extensions.python.message_loop_result._30_repeat_response import RepeatResponse
5
6
7 class FakeAgent:
@@ -23,6 +24,7 @@ class FakeAgent:
24 return {
25 "fw.msg_empty_response.md": "empty",
26 "fw.msg_repeat.md": "repeat",
27 + "fw.msg_repeat_response.md": "Repeated response detected. Retrying.",
28 }[name]
29
30 def hist_add_ai_response(self, response, **kwargs):
@@ -41,7 +43,8 @@ def _run(agent):
43 result_data = {
44 "llm_result": SimpleNamespace(response=agent.response, reasoning=agent.reasoning)
45 }
44 - LoopControl(agent).execute(result_data)
46 + EmptyResponse(agent).execute(result_data)
47 + RepeatResponse(agent).execute(result_data)
48 return result_data
49
50
@@ -51,6 +54,13 @@ def test_empty_result_skips_default_processing():
54 assert _run(agent)["skip_default_processing"] is True
55 assert agent.history == [""]
56 assert agent.warnings == ["empty"]
57 + assert agent.logs == [
58 + {
59 + "type": "warning",
60 + "content": "A0: empty",
61 + "id": "warning",
62 + }
63 + ]
64
65
66 def test_repeat_skips_default_processing():
@@ -67,6 +77,14 @@ def test_repeat_skips_default_processing():
77 ]
78
79
80 +def test_repeat_ignores_reasoning():
81 + response = '{"tool_name":"response"}'
82 + agent = FakeAgent(response, reasoning="thinking", last_response=response)
83 +
84 + assert _run(agent)["skip_default_processing"] is True
85 + assert agent.warnings == ["repeat"]
86 +
87 +
88 def test_result_with_reasoning_uses_default_processing():
89 agent = FakeAgent("", reasoning="thinking")
90