Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 23 additions & 14 deletions libs/code/deepagents_code/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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.
Expand Down
62 changes: 52 additions & 10 deletions libs/code/tests/unit_tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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()."""
Expand Down