From e7e12495859df5abc89bc9e4614cb07148493199 Mon Sep 17 00:00:00 2001 From: "zhangxiaoyu.york" Date: Fri, 10 Apr 2026 12:13:58 +0800 Subject: [PATCH] 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. --- nanobot/agent/loop.py | 12 +++++------ tests/test_truncate_text_shadowing.py | 31 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 tests/test_truncate_text_shadowing.py diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index 16a086fdb..bc83cc77c 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -33,7 +33,7 @@ from nanobot.bus.queue import MessageBus from nanobot.config.schema import AgentDefaults from nanobot.providers.base import LLMProvider 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 if TYPE_CHECKING: @@ -590,7 +590,7 @@ class AgentLoop: self, content: list[dict[str, Any]], *, - truncate_text: bool = False, + should_truncate_text: bool = False, drop_runtime: bool = False, ) -> list[dict[str, Any]]: """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): text = block["text"] - if truncate_text and len(text) > self.max_tool_result_chars: - text = truncate_text(text, self.max_tool_result_chars) + if should_truncate_text and len(text) > self.max_tool_result_chars: + text = truncate_text_fn(text, self.max_tool_result_chars) filtered.append({**block, "text": text}) continue @@ -637,9 +637,9 @@ class AgentLoop: continue # skip empty assistant messages — they poison session context if role == "tool": 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): - filtered = self._sanitize_persisted_blocks(content, truncate_text=True) + filtered = self._sanitize_persisted_blocks(content, should_truncate_text=True) if not filtered: continue entry["content"] = filtered diff --git a/tests/test_truncate_text_shadowing.py b/tests/test_truncate_text_shadowing.py new file mode 100644 index 000000000..11132b511 --- /dev/null +++ b/tests/test_truncate_text_shadowing.py @@ -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"] +