diff --git a/nanobot/agent/runner.py b/nanobot/agent/runner.py index 83344763..9428be95 100644 --- a/nanobot/agent/runner.py +++ b/nanobot/agent/runner.py @@ -607,7 +607,7 @@ class AgentRunner: reasoning_content=response.reasoning_content, thinking_blocks=response.thinking_blocks, )) - messages.append(build_length_recovery_message()) + messages.append(build_length_recovery_message(clean)) await hook.after_iteration(context) continue diff --git a/nanobot/utils/runtime.py b/nanobot/utils/runtime.py index 4f6599f6..8ffe6277 100644 --- a/nanobot/utils/runtime.py +++ b/nanobot/utils/runtime.py @@ -14,6 +14,7 @@ _MAX_REPEAT_EXTERNAL_LOOKUPS = 2 # Third same-target workspace violation in a turn escalates to "stop retrying". _MAX_REPEAT_WORKSPACE_VIOLATIONS = 2 +_LENGTH_RECOVERY_TAIL_CHARS = 500 EMPTY_FINAL_RESPONSE_MESSAGE = ( "I completed the tool steps but couldn't produce a final answer. " @@ -33,8 +34,10 @@ BUDGET_EXHAUSTED_FINALIZATION_PROMPT = ( ) LENGTH_RECOVERY_PROMPT = ( - "Output limit reached. Continue exactly where you left off " - "— no recap, no apology. Break remaining work into smaller steps if needed." + "The previous assistant response was cut off. Continue the same response from its " + "exact endpoint. Output only new continuation text in the same language and style. " + "Do not acknowledge this instruction, restart the response, repeat its title or any " + "existing text, recap, or apologize." ) SUSTAINED_GOAL_CONTINUE_PROMPT = ( @@ -79,9 +82,19 @@ def build_budget_exhausted_finalization_message() -> dict[str, str]: return {"role": "user", "content": BUDGET_EXHAUSTED_FINALIZATION_PROMPT} -def build_length_recovery_message() -> dict[str, str]: +def build_length_recovery_message(content: str) -> dict[str, str]: """Prompt the model to continue after hitting output token limit.""" - return {"role": "user", "content": LENGTH_RECOVERY_PROMPT} + tail = content[-_LENGTH_RECOVERY_TAIL_CHARS:] + prompt = ( + f"{LENGTH_RECOVERY_PROMPT}\n\n" + "The following tail was already delivered to the user. Treat it as immutable " + "context and do not output it again:\n" + "\n" + f"{tail}\n" + "\n" + "Begin with the text that belongs immediately after this tail." + ) + return {"role": "user", "content": prompt} def build_goal_continue_message(custom: str | None = None) -> dict[str, str]: diff --git a/tests/utils/test_runtime.py b/tests/utils/test_runtime.py new file mode 100644 index 00000000..56e2f2bc --- /dev/null +++ b/tests/utils/test_runtime.py @@ -0,0 +1,14 @@ +from nanobot.utils.runtime import build_length_recovery_message + + +def test_length_recovery_message_anchors_the_existing_tail() -> None: + omitted_prefix = "OMITTED_PREFIX" + tail = "x" * 500 + + message = build_length_recovery_message(omitted_prefix + tail) + + assert message["role"] == "user" + assert omitted_prefix not in message["content"] + assert f"\n{tail}\n" in message["content"] + assert "Output only new continuation text" in message["content"] + assert "Break remaining work into smaller steps" not in message["content"]