fix(agent): anchor truncated response continuations
This commit is contained in:
@@ -607,7 +607,7 @@ class AgentRunner:
|
|||||||
reasoning_content=response.reasoning_content,
|
reasoning_content=response.reasoning_content,
|
||||||
thinking_blocks=response.thinking_blocks,
|
thinking_blocks=response.thinking_blocks,
|
||||||
))
|
))
|
||||||
messages.append(build_length_recovery_message())
|
messages.append(build_length_recovery_message(clean))
|
||||||
await hook.after_iteration(context)
|
await hook.after_iteration(context)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ _MAX_REPEAT_EXTERNAL_LOOKUPS = 2
|
|||||||
|
|
||||||
# Third same-target workspace violation in a turn escalates to "stop retrying".
|
# Third same-target workspace violation in a turn escalates to "stop retrying".
|
||||||
_MAX_REPEAT_WORKSPACE_VIOLATIONS = 2
|
_MAX_REPEAT_WORKSPACE_VIOLATIONS = 2
|
||||||
|
_LENGTH_RECOVERY_TAIL_CHARS = 500
|
||||||
|
|
||||||
EMPTY_FINAL_RESPONSE_MESSAGE = (
|
EMPTY_FINAL_RESPONSE_MESSAGE = (
|
||||||
"I completed the tool steps but couldn't produce a final answer. "
|
"I completed the tool steps but couldn't produce a final answer. "
|
||||||
@@ -33,8 +34,10 @@ BUDGET_EXHAUSTED_FINALIZATION_PROMPT = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
LENGTH_RECOVERY_PROMPT = (
|
LENGTH_RECOVERY_PROMPT = (
|
||||||
"Output limit reached. Continue exactly where you left off "
|
"The previous assistant response was cut off. Continue the same response from its "
|
||||||
"— no recap, no apology. Break remaining work into smaller steps if needed."
|
"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 = (
|
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}
|
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."""
|
"""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"
|
||||||
|
"<already_delivered_tail>\n"
|
||||||
|
f"{tail}\n"
|
||||||
|
"</already_delivered_tail>\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]:
|
def build_goal_continue_message(custom: str | None = None) -> dict[str, str]:
|
||||||
|
|||||||
@@ -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"<already_delivered_tail>\n{tail}\n</already_delivered_tail>" in message["content"]
|
||||||
|
assert "Output only new continuation text" in message["content"]
|
||||||
|
assert "Break remaining work into smaller steps" not in message["content"]
|
||||||
Reference in New Issue
Block a user