Surface output-limit truncation instead of misformat retries

Record the Chat Completions finish_reason on LLMResult and add a
message_loop_result extension that, when a turn stopped at the output
token limit (finish_reason "length"/"max_tokens") without a complete
tool request, retries with a warning naming truncation as the cause
rather than the generic misformat message. The warning counts toward
the existing unusable-response loop limit.

Context Doctor now leaves results already handled by an earlier
extension untouched, so truncated envelopes are not repaired and
dispatched with cut-off arguments.

Fixes #1836
This commit is contained in:
Mohammad Hijjawi 2026-09-25 23:59:51 +01:00 • committed by Alessandro
parent ff2a5b9c9c
commit 2f8a06515e
17 changed files with 166 additions and 3 deletions

View file

@ -25,6 +25,7 @@ class StopUnusableResponseLoop(Extension):
self.agent.read_prompt("fw.msg_empty_response.md"),
self.agent.read_prompt("fw.msg_reasoning_only.md"),
self.agent.read_prompt("fw.msg_thoughts_fallback.md"),
self.agent.read_prompt("fw.msg_output_limit.md"),
}:
return

View file

@ -7,6 +7,7 @@
## Ownership
- Extensions receive mutable `result_data` with `llm_result` and may set `skip_default_processing` after fully handling the turn.
- `_05_truncated_response.py` retries turns the provider cut off at the output token limit (`LLMResult.truncated`) that carry no complete tool request: add an agent-facing `fw.msg_output_limit.md` history warning with a separate `fw.msg_output_limit_response.md` user notice naming the finish reason, instead of the generic misformat warning, and count them through the stop-unusable-response-loop extension.
- `_20_empty_response.py` retries fully empty turns (no response and no reasoning): count them toward the unusable-response limit without adding a warning to model history, and use `fw.msg_empty_response.md` for agent-prefixed UI warning text only.
- `_20_empty_response.py` retries reasoning-only turns: add an agent-facing `fw.msg_reasoning_only.md` history warning with a separate `fw.msg_reasoning_only_response.md` user notice, and count them through the stop-unusable-response-loop extension.
- `_30_repeat_response.py` compares `LLMResult.repeat_response_text()` for native calls (public text plus all calls), otherwise full response text. It retries content that exactly matches `loop_data.last_response`, regardless of separate reasoning or provider IDs, using `fw.msg_repeat.md` for history and `fw.msg_repeat_response.md` for the agent-prefixed UI warning text. State advancement is owned by `hist_add_ai_response`; do not call `_remember_llm_result_state` manually.

View file

@ -0,0 +1,41 @@
"""Retry model turns cut off by the output token limit with a specific warning."""
from __future__ import annotations
from typing import Any
from helpers import extract_tools
from helpers.extension import Extension
from helpers.print_style import PrintStyle
class TruncatedResponse(Extension):
def execute(self, result_data: dict[str, Any] | None = None, **kwargs: Any) -> None:
if not self.agent or not isinstance(result_data, dict):
return
if result_data.get("skip_default_processing"):
return
llm_result = result_data.get("llm_result")
if not getattr(llm_result, "truncated", False):
return
if getattr(llm_result, "function_calls", None):
return
response = getattr(llm_result, "response", "")
if isinstance(response, str) and extract_tools.extract_tool_request(response):
return
# Name truncation as the cause instead of the generic misformat warning.
warning = self.agent.read_prompt("fw.msg_output_limit.md")
warning_message = self.agent.hist_add_warning(message=warning)
notice = self.agent.read_prompt(
"fw.msg_output_limit_response.md",
finish_reason=getattr(llm_result, "finish_reason", ""),
)
PrintStyle(font_color="orange", padding=True).print(notice)
self.agent.context.log.log(
type="warning",
content=f"{self.agent.agent_name}: {notice}",
id=warning_message.id,
)
result_data["skip_default_processing"] = True