diff --git a/tests/tools/test_registry.py b/tests/tools/test_registry.py index 6cefab0fc7bd5..6573941fdafc8 100644 --- a/tests/tools/test_registry.py +++ b/tests/tools/test_registry.py @@ -237,6 +237,27 @@ class TestDispatchBoundsDirectErrorResults: assert reg.dispatch("nested", {}) == payload +class TestDispatchExceptionLogging: + def test_raising_handler_logs_bounded_message(self, caplog): + import logging + body = "upstream said: " + "Q" * 200_000 + reg = ToolRegistry() + reg.register( + name="boom", + toolset="core", + schema=_make_schema("boom"), + handler=lambda args, **kw: (_ for _ in ()).throw(RuntimeError(body)), + ) + with caplog.at_level(logging.ERROR, logger="tools.registry"): + result = json.loads(reg.dispatch("boom", {})) + messages = [r.getMessage() for r in caplog.records] + assert messages, "dispatch should log the failure" + for message in messages: + assert len(message) < _MAX_LOGGED_ERROR_CHARS + 200 + assert body not in message + assert len(result["error"]) < _MAX_TOOL_ERROR_CHARS + 200 + + class TestToolsetAvailability: def test_no_check_fn_is_available(self): reg = ToolRegistry() diff --git a/tools/registry.py b/tools/registry.py index 333922d2d0fbb..fc5f98ec23bf8 100644 --- a/tools/registry.py +++ b/tools/registry.py @@ -818,7 +818,10 @@ class ToolRegistry: result = entry.handler(args, **kwargs) return self._normalize_handler_result(name, result) except Exception as e: - logger.exception("Tool %s dispatch error: %s", name, e) + # exc_info already renders the exception, so keep the message copy bounded. + logger.exception( + "Tool %s dispatch error: %s", name, _bound_error_text(str(e)) + ) # Route through the sanitizer so framing tokens / CDATA / fences # in exception strings don't reach the model as structural noise. # See model_tools._sanitize_tool_error for rationale.