fix(agent): avoid truncate_text name shadowing

Rename the boolean flag in _sanitize_persisted_blocks and alias the imported helper so session persistence cannot crash with TypeError when truncation is enabled.
This commit is contained in:
zhangxiaoyu.york 2026-04-10 12:13:58 +08:00 committed by Xubin Ren
parent 2bef9cb650
commit e7e1249585
2 changed files with 37 additions and 6 deletions

View File

@ -33,7 +33,7 @@ from nanobot.bus.queue import MessageBus
from nanobot.config.schema import AgentDefaults from nanobot.config.schema import AgentDefaults
from nanobot.providers.base import LLMProvider from nanobot.providers.base import LLMProvider
from nanobot.session.manager import Session, SessionManager from nanobot.session.manager import Session, SessionManager
from nanobot.utils.helpers import image_placeholder_text, truncate_text from nanobot.utils.helpers import image_placeholder_text, truncate_text as truncate_text_fn
from nanobot.utils.runtime import EMPTY_FINAL_RESPONSE_MESSAGE from nanobot.utils.runtime import EMPTY_FINAL_RESPONSE_MESSAGE
if TYPE_CHECKING: if TYPE_CHECKING:
@ -590,7 +590,7 @@ class AgentLoop:
self, self,
content: list[dict[str, Any]], content: list[dict[str, Any]],
*, *,
truncate_text: bool = False, should_truncate_text: bool = False,
drop_runtime: bool = False, drop_runtime: bool = False,
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
"""Strip volatile multimodal payloads before writing session history.""" """Strip volatile multimodal payloads before writing session history."""
@ -618,8 +618,8 @@ class AgentLoop:
if block.get("type") == "text" and isinstance(block.get("text"), str): if block.get("type") == "text" and isinstance(block.get("text"), str):
text = block["text"] text = block["text"]
if truncate_text and len(text) > self.max_tool_result_chars: if should_truncate_text and len(text) > self.max_tool_result_chars:
text = truncate_text(text, self.max_tool_result_chars) text = truncate_text_fn(text, self.max_tool_result_chars)
filtered.append({**block, "text": text}) filtered.append({**block, "text": text})
continue continue
@ -637,9 +637,9 @@ class AgentLoop:
continue # skip empty assistant messages — they poison session context continue # skip empty assistant messages — they poison session context
if role == "tool": if role == "tool":
if isinstance(content, str) and len(content) > self.max_tool_result_chars: if isinstance(content, str) and len(content) > self.max_tool_result_chars:
entry["content"] = truncate_text(content, self.max_tool_result_chars) entry["content"] = truncate_text_fn(content, self.max_tool_result_chars)
elif isinstance(content, list): elif isinstance(content, list):
filtered = self._sanitize_persisted_blocks(content, truncate_text=True) filtered = self._sanitize_persisted_blocks(content, should_truncate_text=True)
if not filtered: if not filtered:
continue continue
entry["content"] = filtered entry["content"] = filtered

View File

@ -0,0 +1,31 @@
import inspect
from types import SimpleNamespace
def test_sanitize_persisted_blocks_truncate_text_shadowing_regression() -> None:
"""Regression: avoid bool param shadowing imported truncate_text.
Buggy behavior (historical):
- loop.py imports `truncate_text` from helpers
- `_sanitize_persisted_blocks(..., truncate_text: bool=...)` uses same name
- when called with `truncate_text=True`, function body executes `truncate_text(text, ...)`
which resolves to bool and raises `TypeError: 'bool' object is not callable`.
This test asserts the fixed API exists and truncation works without raising.
"""
from nanobot.agent.loop import AgentLoop
sig = inspect.signature(AgentLoop._sanitize_persisted_blocks)
assert "should_truncate_text" in sig.parameters
assert "truncate_text" not in sig.parameters
dummy = SimpleNamespace(max_tool_result_chars=5)
content = [{"type": "text", "text": "0123456789"}]
out = AgentLoop._sanitize_persisted_blocks(dummy, content, should_truncate_text=True)
assert isinstance(out, list)
assert out and out[0]["type"] == "text"
assert isinstance(out[0]["text"], str)
assert out[0]["text"] != content[0]["text"]