diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py
index 6ebebcd..d644845 100644
--- a/nanobot/agent/loop.py
+++ b/nanobot/agent/loop.py
@@ -163,13 +163,7 @@ class AgentLoop:
"""Remove … blocks that some models embed in content."""
if not text:
return None
- # Remove complete think blocks (non-greedy)
- cleaned = re.sub(r"[\s\S]*?", "", text)
- # Remove any stray closing tags left without opening
- cleaned = re.sub(r"", "", cleaned)
- # Remove any stray opening tag and everything after it (incomplete block)
- cleaned = re.sub(r"[\s\S]*$", "", cleaned)
- return cleaned.strip() or None
+ return re.sub(r"[\s\S]*?", "", text).strip() or None
@staticmethod
def _tool_hint(tool_calls: list) -> str:
diff --git a/nanobot/channels/feishu.py b/nanobot/channels/feishu.py
index e3ab8a0..f657359 100644
--- a/nanobot/channels/feishu.py
+++ b/nanobot/channels/feishu.py
@@ -1137,6 +1137,52 @@ class FeishuChannel(BaseChannel):
logger.debug("Bot entered p2p chat (user opened chat window)")
pass
+ @staticmethod
+ def _format_tool_hint_lines(tool_hint: str) -> str:
+ """Split tool hints across lines on top-level call separators only."""
+ parts: list[str] = []
+ buf: list[str] = []
+ depth = 0
+ in_string = False
+ quote_char = ""
+ escaped = False
+
+ for i, ch in enumerate(tool_hint):
+ buf.append(ch)
+
+ if in_string:
+ if escaped:
+ escaped = False
+ elif ch == "\\":
+ escaped = True
+ elif ch == quote_char:
+ in_string = False
+ continue
+
+ if ch in {'"', "'"}:
+ in_string = True
+ quote_char = ch
+ continue
+
+ if ch == "(":
+ depth += 1
+ continue
+
+ if ch == ")" and depth > 0:
+ depth -= 1
+ continue
+
+ if ch == "," and depth == 0:
+ next_char = tool_hint[i + 1] if i + 1 < len(tool_hint) else ""
+ if next_char == " ":
+ parts.append("".join(buf).rstrip())
+ buf = []
+
+ if buf:
+ parts.append("".join(buf).strip())
+
+ return "\n".join(part for part in parts if part)
+
async def _send_tool_hint_card(self, receive_id_type: str, receive_id: str, tool_hint: str) -> None:
"""Send tool hint as an interactive card with formatted code block.
@@ -1147,9 +1193,8 @@ class FeishuChannel(BaseChannel):
"""
loop = asyncio.get_running_loop()
- # Format: put each tool call on its own line for readability
- # _tool_hint joins multiple calls with ", "
- formatted_code = tool_hint.replace(", ", ",\n") if ", " in tool_hint else tool_hint
+ # Put each top-level tool call on its own line without altering commas inside arguments.
+ formatted_code = self._format_tool_hint_lines(tool_hint)
card = {
"config": {"wide_screen_mode": True},
diff --git a/tests/test_feishu_tool_hint_code_block.py b/tests/test_feishu_tool_hint_code_block.py
index a3fc024..2a1b812 100644
--- a/tests/test_feishu_tool_hint_code_block.py
+++ b/tests/test_feishu_tool_hint_code_block.py
@@ -114,3 +114,25 @@ async def test_tool_hint_multiple_tools_in_one_message(mock_feishu_channel):
# Each tool call should be on its own line
expected_md = "**Tool Calls**\n\n```text\nweb_search(\"query\"),\nread_file(\"/path/to/file\")\n```"
assert content["elements"][0]["content"] == expected_md
+
+
+@mark.asyncio
+async def test_tool_hint_keeps_commas_inside_arguments(mock_feishu_channel):
+ """Commas inside a single tool argument must not be split onto a new line."""
+ msg = OutboundMessage(
+ channel="feishu",
+ chat_id="oc_123456",
+ content='web_search("foo, bar"), read_file("/path/to/file")',
+ metadata={"_tool_hint": True}
+ )
+
+ with patch.object(mock_feishu_channel, '_send_message_sync') as mock_send:
+ await mock_feishu_channel.send(msg)
+
+ content = json.loads(mock_send.call_args[0][3])
+ expected_md = (
+ "**Tool Calls**\n\n```text\n"
+ "web_search(\"foo, bar\"),\n"
+ "read_file(\"/path/to/file\")\n```"
+ )
+ assert content["elements"][0]["content"] == expected_md