mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-21 17:25:26 -04:00
4f94a30c11
Deep Agents Code now includes an in-app Debug Console. Press `Ctrl+\` to inspect session details and filter, view, or copy recent application logs without enabling file-based debug logging. --- Debugging client-side TUI problems currently requires restarting with file logging enabled and tailing a log in another terminal. This adds an in-app Debug Console, modeled on Mistral Vibe's console, so users can inspect session state and recent application logs without leaving the TUI. Open the console with `Ctrl+\` or the hidden `/debug` command. It shows a point-in-time snapshot of the version, model, thread, working directory, auto-approve state, sandbox, MCP servers, token usage, and debug-log path. Below that, it tails recent `deepagents_code.*` records from an always-on bounded in-memory buffer and supports filtering by severity. The buffer captures `INFO` and above by default, or the level selected with `DEEPAGENTS_CODE_LOG_LEVEL`. `DEEPAGENTS_CODE_DEBUG` continues to enable append-only file logging and defaults capture to `DEBUG`. `Ctrl+L` clears the current console view without clearing the underlying buffer; `c` copies the visible filtered records retained since the last clear, and clicking a record copies only that record. --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
149 lines
5.6 KiB
Python
149 lines
5.6 KiB
Python
"""Shared debug-logging configuration for runtime and file-based tracing.
|
|
|
|
When the `DEEPAGENTS_CODE_DEBUG` environment variable is set, modules that handle
|
|
streaming or remote communication can enable detailed file-based logging. This
|
|
helper centralizes the setup so the env-var names, file path, log level, and
|
|
format are defined in one place.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from deepagents_code._env_vars import (
|
|
DEBUG,
|
|
DEBUG_FILE,
|
|
DEFAULT_DEBUG_FILE,
|
|
LOG_LEVEL,
|
|
is_env_truthy,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_DEBUG_HANDLER_ATTR = "_deepagents_code_debug_handler"
|
|
LOG_LEVELS = {
|
|
"DEBUG": logging.DEBUG,
|
|
"INFO": logging.INFO,
|
|
"WARNING": logging.WARNING,
|
|
"ERROR": logging.ERROR,
|
|
"CRITICAL": logging.CRITICAL,
|
|
}
|
|
"""Canonical level-name to `logging` level mapping.
|
|
|
|
The single source of truth for level names and their numeric values, shared with
|
|
the Debug Console's level filter so severity ordering is never re-derived from
|
|
hardcoded integers.
|
|
"""
|
|
|
|
|
|
def resolve_log_level(*, debug_enabled: bool | None = None) -> int:
|
|
"""Resolve the configured runtime logging level.
|
|
|
|
Args:
|
|
debug_enabled: Whether `DEEPAGENTS_CODE_DEBUG` is truthy. When omitted,
|
|
the current environment is checked.
|
|
|
|
Returns:
|
|
A standard `logging` level integer. Defaults to `DEBUG` when debug file
|
|
logging is enabled and `INFO` otherwise.
|
|
"""
|
|
if debug_enabled is None:
|
|
debug_enabled = is_env_truthy(DEBUG)
|
|
fallback = logging.DEBUG if debug_enabled else logging.INFO
|
|
raw = os.environ.get(LOG_LEVEL)
|
|
if raw is None or not raw.strip():
|
|
return fallback
|
|
level = LOG_LEVELS.get(raw.strip().upper())
|
|
if level is not None:
|
|
return level
|
|
valid = ", ".join(LOG_LEVELS)
|
|
message = f"ignoring invalid {LOG_LEVEL}={raw!r}; expected one of {valid}"
|
|
# stderr for headless / pre-TUI visibility; the logger so it also lands in
|
|
# the always-on in-memory buffer and surfaces in the Debug Console (the
|
|
# buffer is installed before this runs; see __init__.py).
|
|
print(f"Warning: {message}", file=sys.stderr) # noqa: T201
|
|
logger.warning("%s", message)
|
|
return fallback
|
|
|
|
|
|
def configure_debug_logging(target: logging.Logger) -> None:
|
|
"""Configure runtime log level and optional file logging for *target*.
|
|
|
|
Intended to be called once on the `deepagents_code` package logger; child
|
|
module loggers reach the same handlers via propagation, so individual modules
|
|
do not configure logging themselves.
|
|
|
|
`DEEPAGENTS_CODE_LOG_LEVEL` controls the package logger level independently of
|
|
file logging. If it is unset, `DEEPAGENTS_CODE_DEBUG=1` defaults to `DEBUG`;
|
|
otherwise the runtime level defaults to `INFO`.
|
|
|
|
When `DEEPAGENTS_CODE_DEBUG` is truthy, a file handler is attached. The log
|
|
file defaults to `DEFAULT_DEBUG_FILE` but can be overridden with
|
|
`DEEPAGENTS_CODE_DEBUG_FILE`. The handler appends (`mode='a'`) so logs are
|
|
preserved across separate process runs. Calling this again with the same
|
|
resolved path does not stack duplicate handlers: the existing tagged handler
|
|
is reused and its level re-applied. If the resolved path changes, the stale
|
|
handler is closed and replaced.
|
|
|
|
Args:
|
|
target: Logger to configure.
|
|
"""
|
|
debug_enabled = is_env_truthy(DEBUG)
|
|
level = resolve_log_level(debug_enabled=debug_enabled)
|
|
target.setLevel(level)
|
|
|
|
if not debug_enabled:
|
|
return
|
|
|
|
debug_path = Path(os.environ.get(DEBUG_FILE, DEFAULT_DEBUG_FILE))
|
|
for existing in list(target.handlers):
|
|
if not (
|
|
isinstance(existing, logging.FileHandler)
|
|
and getattr(existing, _DEBUG_HANDLER_ATTR, False)
|
|
):
|
|
continue
|
|
if Path(existing.baseFilename) == debug_path:
|
|
existing.setLevel(level)
|
|
return
|
|
# The debug path changed; drop the stale handler before re-attaching so
|
|
# we don't leak its file descriptor or fan logs out to two files.
|
|
target.removeHandler(existing)
|
|
existing.close()
|
|
|
|
try:
|
|
handler = logging.FileHandler(str(debug_path), mode="a")
|
|
except OSError as exc:
|
|
message = f"could not open debug log file {debug_path}: {exc}"
|
|
# stderr for headless / pre-TUI visibility; the logger so it also lands
|
|
# in the always-on in-memory buffer and surfaces in the Debug Console.
|
|
print(f"Warning: {message}", file=sys.stderr) # noqa: T201
|
|
logger.warning("%s", message)
|
|
return
|
|
setattr(handler, _DEBUG_HANDLER_ATTR, True)
|
|
handler.setLevel(level)
|
|
handler.setFormatter(logging.Formatter("%(asctime)s %(name)s %(message)s"))
|
|
target.addHandler(handler)
|
|
|
|
|
|
def installed_debug_log_path() -> Path | None:
|
|
"""Return the path of the active debug log file, or `None` if not logging.
|
|
|
|
Reflects the file handler actually attached by `configure_debug_logging`,
|
|
not the current `DEEPAGENTS_CODE_DEBUG` env value. The two diverge when the
|
|
variable is set after import — e.g. via a project/global `.env` loaded during
|
|
settings bootstrap — in which case the variable reads truthy but no handler
|
|
was installed and no log file exists. Callers that surface "full error in
|
|
<path>" hints must use this rather than the env var to avoid pointing users
|
|
at a file that was never created.
|
|
"""
|
|
package_logger = logging.getLogger(__package__ or "deepagents_code")
|
|
for handler in package_logger.handlers:
|
|
if isinstance(handler, logging.FileHandler) and getattr(
|
|
handler, _DEBUG_HANDLER_ATTR, False
|
|
):
|
|
return Path(handler.baseFilename)
|
|
return None
|