mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-05-19 16:12:30 +00:00
feat(config): add toolHintMaxLength to control tool hint truncation
Add to config (default: 40, range: 20-500).
Controls how many characters of tool hints are shown in progress updates
(e.g. '$ cd …/project && npm test').
Set to 120+ to see full commands instead of truncated hints:
```json
{
"agents": {
"defaults": {
"toolHintMaxLength": 120
}
}
}
```
- Thread max_length through format_tool_hints → _fmt_known/_fmt_mcp/_fmt_fallback
- Make path abbreviation in _abbreviate_command proportional to max_length
- Add TestToolHintMaxLength test class with 5 tests
- All 41 existing tests pass
This commit is contained in:
parent
3baa869fdb
commit
f256d7ab9b
@ -237,6 +237,7 @@ class AgentLoop:
|
||||
else defaults.max_tool_result_chars
|
||||
)
|
||||
self.provider_retry_mode = provider_retry_mode
|
||||
self.tool_hint_max_length = defaults.tool_hint_max_length
|
||||
self.web_config = web_config or WebToolsConfig()
|
||||
self.exec_config = exec_config or ExecToolConfig()
|
||||
self.cron_service = cron_service
|
||||
@ -466,12 +467,11 @@ class AgentLoop:
|
||||
"""Return the chat id shown in runtime metadata for the model."""
|
||||
return str(msg.metadata.get("context_chat_id") or msg.chat_id)
|
||||
|
||||
@staticmethod
|
||||
def _tool_hint(tool_calls: list) -> str:
|
||||
def _tool_hint(self, tool_calls: list) -> str:
|
||||
"""Format tool calls as concise hints with smart abbreviation."""
|
||||
from nanobot.utils.tool_hints import format_tool_hints
|
||||
|
||||
return format_tool_hints(tool_calls)
|
||||
return format_tool_hints(tool_calls, max_length=self.tool_hint_max_length)
|
||||
|
||||
async def _dispatch_command_inline(
|
||||
self,
|
||||
|
||||
@ -81,6 +81,13 @@ class AgentDefaults(Base):
|
||||
max_concurrent_subagents: int = Field(default=1, ge=1)
|
||||
max_tool_result_chars: int = 16_000
|
||||
provider_retry_mode: Literal["standard", "persistent"] = "standard"
|
||||
tool_hint_max_length: int = Field(
|
||||
default=40,
|
||||
ge=20,
|
||||
le=500,
|
||||
validation_alias=AliasChoices("toolHintMaxLength"),
|
||||
serialization_alias="toolHintMaxLength",
|
||||
) # Max characters for tool hint display (e.g. "$ cd …/project && npm test")
|
||||
reasoning_effort: str | None = None # low / medium / high / adaptive - enables LLM thinking mode
|
||||
timezone: str = "UTC" # IANA timezone, e.g. "Asia/Shanghai", "America/New_York"
|
||||
unified_session: bool = False # Share one session across all channels (single-user multi-device)
|
||||
|
||||
@ -27,7 +27,7 @@ _PATH_IN_CMD_RE = re.compile(
|
||||
)
|
||||
|
||||
|
||||
def format_tool_hints(tool_calls: list) -> str:
|
||||
def format_tool_hints(tool_calls: list, max_length: int = 40) -> str:
|
||||
"""Format tool calls as concise hints with smart abbreviation."""
|
||||
if not tool_calls:
|
||||
return ""
|
||||
@ -36,11 +36,11 @@ def format_tool_hints(tool_calls: list) -> str:
|
||||
for tc in tool_calls:
|
||||
fmt = _TOOL_FORMATS.get(tc.name)
|
||||
if fmt:
|
||||
formatted.append(_fmt_known(tc, fmt))
|
||||
formatted.append(_fmt_known(tc, fmt, max_length))
|
||||
elif tc.name.startswith("mcp_"):
|
||||
formatted.append(_fmt_mcp(tc))
|
||||
formatted.append(_fmt_mcp(tc, max_length))
|
||||
else:
|
||||
formatted.append(_fmt_fallback(tc))
|
||||
formatted.append(_fmt_fallback(tc, max_length))
|
||||
|
||||
hints = []
|
||||
for hint in formatted:
|
||||
@ -80,7 +80,7 @@ def _extract_arg(tc, key_args: list[str]) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _fmt_known(tc, fmt: tuple) -> str:
|
||||
def _fmt_known(tc, fmt: tuple, max_length: int = 40) -> str:
|
||||
"""Format a registered tool using its template."""
|
||||
val = _extract_arg(tc, fmt[0])
|
||||
if val is None:
|
||||
@ -88,18 +88,20 @@ def _fmt_known(tc, fmt: tuple) -> str:
|
||||
if fmt[2]: # is_path
|
||||
val = abbreviate_path(val)
|
||||
elif fmt[3]: # is_command
|
||||
val = _abbreviate_command(val)
|
||||
val = _abbreviate_command(val, max_len=max_length)
|
||||
return fmt[1].format(val)
|
||||
|
||||
|
||||
def _abbreviate_command(cmd: str, max_len: int = 40) -> str:
|
||||
"""Abbreviate paths in a command string, then truncate."""
|
||||
path_max = max(max_len // 2, 25)
|
||||
|
||||
def _replace_path(match: re.Match[str]) -> str:
|
||||
if match.group("double") is not None:
|
||||
return f'"{abbreviate_path(match.group("double"), max_len=25)}"'
|
||||
return f'"{abbreviate_path(match.group("double"), max_len=path_max)}"'
|
||||
if match.group("single") is not None:
|
||||
return f"'{abbreviate_path(match.group('single'), max_len=25)}'"
|
||||
return abbreviate_path(match.group("bare"), max_len=25)
|
||||
return f"'{abbreviate_path(match.group('single'), max_len=path_max)}'"
|
||||
return abbreviate_path(match.group("bare"), max_len=path_max)
|
||||
|
||||
abbreviated = _PATH_IN_CMD_RE.sub(_replace_path, cmd)
|
||||
if len(abbreviated) <= max_len:
|
||||
@ -107,7 +109,7 @@ def _abbreviate_command(cmd: str, max_len: int = 40) -> str:
|
||||
return abbreviated[:max_len - 1] + "\u2026"
|
||||
|
||||
|
||||
def _fmt_mcp(tc) -> str:
|
||||
def _fmt_mcp(tc, max_length: int = 40) -> str:
|
||||
"""Format MCP tool as server::tool."""
|
||||
name = tc.name
|
||||
if "__" in name:
|
||||
@ -125,13 +127,13 @@ def _fmt_mcp(tc) -> str:
|
||||
val = next((v for v in args.values() if isinstance(v, str) and v), None)
|
||||
if val is None:
|
||||
return f"{server}::{tool}"
|
||||
return f'{server}::{tool}("{abbreviate_path(val, 40)}")'
|
||||
return f'{server}::{tool}("{abbreviate_path(val, max_length)}")'
|
||||
|
||||
|
||||
def _fmt_fallback(tc) -> str:
|
||||
def _fmt_fallback(tc, max_length: int = 40) -> str:
|
||||
"""Original formatting logic for unregistered tools."""
|
||||
args = _get_args(tc)
|
||||
val = next(iter(args.values()), None) if isinstance(args, dict) else None
|
||||
if not isinstance(val, str):
|
||||
return tc.name
|
||||
return f'{tc.name}("{abbreviate_path(val, 40)}")' if len(val) > 40 else f'{tc.name}("{val}")'
|
||||
return f'{tc.name}("{abbreviate_path(val, max_length)}")' if len(val) > max_length else f'{tc.name}("{val}")'
|
||||
|
||||
@ -8,9 +8,9 @@ def _tc(name: str, args) -> ToolCallRequest:
|
||||
return ToolCallRequest(id="c1", name=name, arguments=args)
|
||||
|
||||
|
||||
def _hint(calls):
|
||||
def _hint(calls, max_length=40):
|
||||
"""Shortcut for format_tool_hints."""
|
||||
return format_tool_hints(calls)
|
||||
return format_tool_hints(calls, max_length=max_length)
|
||||
|
||||
|
||||
class TestToolHintKnownTools:
|
||||
@ -254,3 +254,38 @@ class TestToolHintMixedFolding:
|
||||
assert "\u00d7" not in result
|
||||
parts = result.split(", ")
|
||||
assert len(parts) == 5
|
||||
|
||||
|
||||
class TestToolHintMaxLength:
|
||||
"""Test max_length parameter controls truncation of tool hints."""
|
||||
|
||||
def test_exec_default_truncates_at_40(self):
|
||||
cmd = "cd /very/long/path/to/some/project && npm run build && npm test"
|
||||
result = _hint([_tc("exec", {"command": cmd})], max_length=40)
|
||||
assert len(result) <= 50 # "$ " prefix + 40 + ellipsis
|
||||
assert "\u2026" in result
|
||||
|
||||
def test_exec_larger_max_length_shows_more(self):
|
||||
cmd = "cd /very/long/path/to/some/project && npm run build && npm test"
|
||||
short = _hint([_tc("exec", {"command": cmd})], max_length=40)
|
||||
long = _hint([_tc("exec", {"command": cmd})], max_length=120)
|
||||
assert len(long) > len(short)
|
||||
assert "npm test" in long
|
||||
|
||||
def test_exec_max_length_120_shows_full_command(self):
|
||||
cmd = "cd /home/user/project && npm install && npm run build"
|
||||
result = _hint([_tc("exec", {"command": cmd})], max_length=120)
|
||||
assert "npm run build" in result
|
||||
|
||||
def test_fallback_respects_max_length(self):
|
||||
long_val = "a" * 100
|
||||
result = _hint([_tc("custom_tool", {"data": long_val})], max_length=60)
|
||||
assert "\u2026" in result
|
||||
result_40 = _hint([_tc("custom_tool", {"data": long_val})], max_length=40)
|
||||
assert len(result) > len(result_40)
|
||||
|
||||
def test_mcp_respects_max_length(self):
|
||||
long_url = "https://example.com/very/long/path/to/resource"
|
||||
result = _hint([_tc("mcp_github__fetch", {"url": long_url})], max_length=80)
|
||||
result_40 = _hint([_tc("mcp_github__fetch", {"url": long_url})], max_length=40)
|
||||
assert len(result) >= len(result_40)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user