diff --git a/libs/code/deepagents_code/config.py b/libs/code/deepagents_code/config.py index d39954fe040..169caa178df 100644 --- a/libs/code/deepagents_code/config.py +++ b/libs/code/deepagents_code/config.py @@ -509,20 +509,29 @@ class _LangSmithProfileConfig(Protocol): """OAuth refresh token from the active LangSmith profile.""" -def _quiet_sdk_tracing_logging() -> None: - """Keep LangSmith/LangChain SDK logging from corrupting the TUI. - - These SDK loggers emit ingestion/auth errors (e.g. repeated 401s) on their - own loggers. With no handler attached they reach Python's last-resort stderr - handler and bleed onto the alternate-screen TUI. Route them to the debug log - when `DEEPAGENTS_CODE_DEBUG` is set, otherwise attach a `NullHandler` so they - stay off the terminal. +_QUIET_SDK_LOGGER_NAMES = ( + "deepagents.profiles.harness.harness_profiles", + "langchain", + "langsmith", +) + + +def _quiet_sdk_logging() -> None: + """Keep non-actionable SDK diagnostics off the terminal. + + The harness-profile resolver and tracing SDKs emit diagnostics on their own + logger hierarchies. With no handler attached, warnings reach Python's + last-resort stderr handler and can bleed into command output or the + alternate-screen TUI. Route them to the debug log when + `DEEPAGENTS_CODE_DEBUG` is set, otherwise attach a `NullHandler` so they stay + off the terminal. Other Deep Agents loggers remain untouched so actionable + runtime warnings are still visible. """ from deepagents_code._debug import configure_debug_logging from deepagents_code._env_vars import DEBUG, is_env_truthy debug_enabled = is_env_truthy(DEBUG) - for name in ("langsmith", "langchain"): + for name in _QUIET_SDK_LOGGER_NAMES: sdk_logger = logging.getLogger(name) if debug_enabled: configure_debug_logging(sdk_logger) @@ -674,7 +683,7 @@ def _disable_orphaned_tracing() -> None: `api_url`) or replica endpoints (`LANGSMITH_RUNS_ENDPOINTS`/ `LANGCHAIN_RUNS_ENDPOINTS`) signal tracing can upload without a top-level API key, so those explicitly configured targets are trusted and left alone. - The SDK loggers are quieted separately by `_quiet_sdk_tracing_logging`, so + The SDK loggers are quieted separately by `_quiet_sdk_logging`, so any residual ingest errors stay off the TUI. """ global _orphaned_tracing_disabled_notice # noqa: PLW0603 @@ -773,7 +782,7 @@ def _apply_stored_langsmith_tracing(*, replace_project: bool = False) -> None: startup-perf budget). So a stored-but-invalid key (typo'd, revoked, or for the wrong workspace) still force-enables tracing, and its traces are then silently dropped at ingest with only SDK-internal 401s — which - `_quiet_sdk_tracing_logging` routes away from the TUI. `_disable_orphaned_tracing` + `_quiet_sdk_logging` routes away from the TUI. `_disable_orphaned_tracing` and `consume_orphaned_tracing_disabled_notice` guard only the *absent*-key case, not the invalid-key case. If traces never appear, the key is the first thing to re-check via `/auth`. @@ -926,9 +935,9 @@ def _ensure_bootstrap() -> None: configure_debug_logging(logging.getLogger("deepagents_code")) - # Keep LangSmith/LangChain SDK logging off the TUI (route to the - # debug log when enabled, else swallow via NullHandler). - _quiet_sdk_tracing_logging() + # Keep dependency logging out of command output and the TUI. Route it + # to the debug log when enabled, otherwise swallow it via NullHandler. + _quiet_sdk_logging() # Capture AFTER dotenv loading so .env-only values are visible, # but BEFORE the override below replaces it. diff --git a/libs/code/tests/unit_tests/test_config.py b/libs/code/tests/unit_tests/test_config.py index b5f87a1e251..e9dfa7c39fa 100644 --- a/libs/code/tests/unit_tests/test_config.py +++ b/libs/code/tests/unit_tests/test_config.py @@ -14,6 +14,7 @@ from deepagents_code._env_vars import SERVER_ENV_PREFIX from deepagents_code._version import __version__ from deepagents_code.config import ( + _QUIET_SDK_LOGGER_NAMES, CLI_MAX_RETRIES_KEY, LANGSMITH_EU_ENDPOINT, LANGSMITH_US_ENDPOINT, @@ -29,7 +30,7 @@ _create_model_via_init, _disable_orphaned_tracing, _get_provider_kwargs, - _quiet_sdk_tracing_logging, + _quiet_sdk_logging, _read_config_toml_retries, _resolve_retry_kwargs, _resolve_retry_param_name, @@ -3254,8 +3255,8 @@ def test_enabled_and_explicitly_disabled_is_rejected(self) -> None: ) -class TestQuietSdkTracingLogging: - """Tests for _quiet_sdk_tracing_logging().""" +class TestQuietSdkLogging: + """Tests for _quiet_sdk_logging().""" def test_attaches_null_handler_without_debug( self, monkeypatch: pytest.MonkeyPatch @@ -3264,34 +3265,75 @@ def test_attaches_null_handler_without_debug( from deepagents_code._env_vars import DEBUG monkeypatch.delenv(DEBUG, raising=False) - for name in ("langsmith", "langchain"): + for name in _QUIET_SDK_LOGGER_NAMES: logger = logging.getLogger(name) logger.handlers.clear() logger.setLevel(logging.NOTSET) + monkeypatch.setattr(logger, "propagate", True) - _quiet_sdk_tracing_logging() + _quiet_sdk_logging() - for name in ("langsmith", "langchain"): + for name in _QUIET_SDK_LOGGER_NAMES: logger = logging.getLogger(name) handlers = logger.handlers assert any(isinstance(h, logging.NullHandler) for h in handlers) assert logger.level == logging.NOTSET + # Propagation is left intact so a deliberately configured handler + # (an embedding app's root handler, pytest's caplog) still receives + # real SDK errors; the NullHandler alone keeps routine noise off the + # last-resort stderr handler. + assert logger.propagate is True def test_idempotent(self, monkeypatch: pytest.MonkeyPatch) -> None: """Repeated calls do not stack duplicate handlers.""" from deepagents_code._env_vars import DEBUG monkeypatch.delenv(DEBUG, raising=False) - for name in ("langsmith", "langchain"): + for name in _QUIET_SDK_LOGGER_NAMES: logging.getLogger(name).handlers.clear() - _quiet_sdk_tracing_logging() - _quiet_sdk_tracing_logging() + _quiet_sdk_logging() + _quiet_sdk_logging() - for name in ("langsmith", "langchain"): + for name in _QUIET_SDK_LOGGER_NAMES: handlers = logging.getLogger(name).handlers assert len(handlers) == 1 + def test_routes_harness_diagnostics_to_debug_log( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Debug mode configures a file handler for harness diagnostics.""" + from deepagents_code._env_vars import DEBUG + + monkeypatch.setenv(DEBUG, "1") + harness_logger = logging.getLogger( + "deepagents.profiles.harness.harness_profiles" + ) + harness_logger.handlers.clear() + monkeypatch.setattr(harness_logger, "propagate", True) + + with patch("deepagents_code._debug.configure_debug_logging") as configure: + _quiet_sdk_logging() + + assert any(call.args == (harness_logger,) for call in configure.call_args_list) + assert harness_logger.propagate is True + + def test_leaves_other_deepagents_loggers_untouched( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Actionable Deep Agents runtime warnings keep their normal routing.""" + from deepagents_code._env_vars import DEBUG + + monkeypatch.delenv(DEBUG, raising=False) + runtime_logger = logging.getLogger("deepagents.backends.filesystem") + runtime_logger.handlers.clear() + monkeypatch.setattr(runtime_logger, "propagate", True) + + _quiet_sdk_logging() + + assert runtime_logger.handlers == [] + assert runtime_logger.propagate is True + class TestFetchLangsmithProjectUrl: """Tests for fetch_langsmith_project_url()."""