zjcode 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. deepagents_code/__init__.py +42 -0
  2. deepagents_code/__main__.py +6 -0
  3. deepagents_code/_ask_user_types.py +90 -0
  4. deepagents_code/_cli_context.py +96 -0
  5. deepagents_code/_constants.py +41 -0
  6. deepagents_code/_debug.py +148 -0
  7. deepagents_code/_debug_buffer.py +204 -0
  8. deepagents_code/_env_vars.py +411 -0
  9. deepagents_code/_fake_models.py +66 -0
  10. deepagents_code/_git.py +521 -0
  11. deepagents_code/_paths.py +69 -0
  12. deepagents_code/_server_config.py +576 -0
  13. deepagents_code/_session_stats.py +235 -0
  14. deepagents_code/_startup_error.py +45 -0
  15. deepagents_code/_testing_models.py +305 -0
  16. deepagents_code/_textual_patches.py +420 -0
  17. deepagents_code/_tool_stream.py +694 -0
  18. deepagents_code/_version.py +46 -0
  19. deepagents_code/agent.py +1976 -0
  20. deepagents_code/app.py +19239 -0
  21. deepagents_code/app.tcss +438 -0
  22. deepagents_code/approval_mode.py +131 -0
  23. deepagents_code/ask_user.py +306 -0
  24. deepagents_code/auth_display.py +185 -0
  25. deepagents_code/auth_store.py +545 -0
  26. deepagents_code/built_in_skills/__init__.py +5 -0
  27. deepagents_code/built_in_skills/remember/SKILL.md +118 -0
  28. deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
  29. deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
  30. deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
  31. deepagents_code/client/__init__.py +1 -0
  32. deepagents_code/client/commands/__init__.py +1 -0
  33. deepagents_code/client/commands/auth.py +541 -0
  34. deepagents_code/client/commands/config.py +714 -0
  35. deepagents_code/client/commands/mcp.py +250 -0
  36. deepagents_code/client/commands/tools.py +416 -0
  37. deepagents_code/client/launch/__init__.py +1 -0
  38. deepagents_code/client/launch/server.py +978 -0
  39. deepagents_code/client/launch/server_manager.py +540 -0
  40. deepagents_code/client/non_interactive.py +1758 -0
  41. deepagents_code/client/remote_client.py +794 -0
  42. deepagents_code/clipboard.py +217 -0
  43. deepagents_code/command_registry.py +450 -0
  44. deepagents_code/config.py +4829 -0
  45. deepagents_code/config_manifest.py +1451 -0
  46. deepagents_code/configurable_model.py +577 -0
  47. deepagents_code/default_agent_prompt.md +12 -0
  48. deepagents_code/doctor.py +559 -0
  49. deepagents_code/editor.py +142 -0
  50. deepagents_code/event_bus.py +411 -0
  51. deepagents_code/extras_info.py +661 -0
  52. deepagents_code/file_ops.py +576 -0
  53. deepagents_code/formatting.py +135 -0
  54. deepagents_code/goal_rubric.py +497 -0
  55. deepagents_code/goal_tools.py +459 -0
  56. deepagents_code/hooks.py +348 -0
  57. deepagents_code/input.py +1041 -0
  58. deepagents_code/integrations/__init__.py +1 -0
  59. deepagents_code/integrations/openai_codex.py +551 -0
  60. deepagents_code/integrations/sandbox_config.py +198 -0
  61. deepagents_code/integrations/sandbox_factory.py +1124 -0
  62. deepagents_code/integrations/sandbox_provider.py +137 -0
  63. deepagents_code/integrations/sandbox_registry.py +350 -0
  64. deepagents_code/iterm_cursor_guide.py +176 -0
  65. deepagents_code/local_context.py +926 -0
  66. deepagents_code/main.py +3985 -0
  67. deepagents_code/managed_tools.py +642 -0
  68. deepagents_code/mcp_auth.py +1950 -0
  69. deepagents_code/mcp_config.py +176 -0
  70. deepagents_code/mcp_disabled.py +212 -0
  71. deepagents_code/mcp_login_service.py +281 -0
  72. deepagents_code/mcp_oauth_ui.py +199 -0
  73. deepagents_code/mcp_providers/__init__.py +23 -0
  74. deepagents_code/mcp_providers/_registry.py +39 -0
  75. deepagents_code/mcp_providers/base.py +133 -0
  76. deepagents_code/mcp_providers/github.py +102 -0
  77. deepagents_code/mcp_providers/slack.py +175 -0
  78. deepagents_code/mcp_tools.py +2427 -0
  79. deepagents_code/mcp_trust.py +207 -0
  80. deepagents_code/media_utils.py +826 -0
  81. deepagents_code/memory_guard.py +474 -0
  82. deepagents_code/model_config.py +4156 -0
  83. deepagents_code/notifications.py +247 -0
  84. deepagents_code/offload.py +402 -0
  85. deepagents_code/onboarding.py +223 -0
  86. deepagents_code/output.py +69 -0
  87. deepagents_code/paste_collapse.py +103 -0
  88. deepagents_code/project_utils.py +231 -0
  89. deepagents_code/py.typed +0 -0
  90. deepagents_code/reasoning_effort.py +641 -0
  91. deepagents_code/reliable_rubric.py +97 -0
  92. deepagents_code/resume_state.py +237 -0
  93. deepagents_code/server_graph.py +310 -0
  94. deepagents_code/session_end_summary.py +329 -0
  95. deepagents_code/sessions.py +1716 -0
  96. deepagents_code/skills/__init__.py +18 -0
  97. deepagents_code/skills/commands.py +1252 -0
  98. deepagents_code/skills/invocation.py +112 -0
  99. deepagents_code/skills/load.py +196 -0
  100. deepagents_code/skills/trust.py +547 -0
  101. deepagents_code/state_migration.py +136 -0
  102. deepagents_code/subagents.py +278 -0
  103. deepagents_code/system_prompt.md +204 -0
  104. deepagents_code/terminal_capabilities.py +115 -0
  105. deepagents_code/terminal_escape.py +287 -0
  106. deepagents_code/theme.py +891 -0
  107. deepagents_code/todo_list_prompt.md +12 -0
  108. deepagents_code/tool_catalog.py +509 -0
  109. deepagents_code/tool_display.py +367 -0
  110. deepagents_code/tools.py +516 -0
  111. deepagents_code/tui/__init__.py +1 -0
  112. deepagents_code/tui/textual_adapter.py +2553 -0
  113. deepagents_code/tui/widgets/__init__.py +9 -0
  114. deepagents_code/tui/widgets/_js_eval_display.py +139 -0
  115. deepagents_code/tui/widgets/_links.py +261 -0
  116. deepagents_code/tui/widgets/agent_selector.py +420 -0
  117. deepagents_code/tui/widgets/approval.py +602 -0
  118. deepagents_code/tui/widgets/ask_user.py +515 -0
  119. deepagents_code/tui/widgets/auth.py +1997 -0
  120. deepagents_code/tui/widgets/autocomplete.py +890 -0
  121. deepagents_code/tui/widgets/chat_input.py +3181 -0
  122. deepagents_code/tui/widgets/codex_auth.py +452 -0
  123. deepagents_code/tui/widgets/cwd_switch.py +242 -0
  124. deepagents_code/tui/widgets/debug_console.py +868 -0
  125. deepagents_code/tui/widgets/diff.py +252 -0
  126. deepagents_code/tui/widgets/effort_selector.py +189 -0
  127. deepagents_code/tui/widgets/goal_review.py +390 -0
  128. deepagents_code/tui/widgets/goal_status.py +50 -0
  129. deepagents_code/tui/widgets/history.py +194 -0
  130. deepagents_code/tui/widgets/install_confirm.py +248 -0
  131. deepagents_code/tui/widgets/launch_init.py +482 -0
  132. deepagents_code/tui/widgets/loading.py +227 -0
  133. deepagents_code/tui/widgets/mcp_login.py +539 -0
  134. deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
  135. deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
  136. deepagents_code/tui/widgets/message_store.py +999 -0
  137. deepagents_code/tui/widgets/messages.py +3846 -0
  138. deepagents_code/tui/widgets/model_selector.py +2343 -0
  139. deepagents_code/tui/widgets/notification_center.py +456 -0
  140. deepagents_code/tui/widgets/notification_detail.py +257 -0
  141. deepagents_code/tui/widgets/notification_settings.py +170 -0
  142. deepagents_code/tui/widgets/restart_prompt.py +158 -0
  143. deepagents_code/tui/widgets/skill_trust.py +131 -0
  144. deepagents_code/tui/widgets/startup_tip.py +91 -0
  145. deepagents_code/tui/widgets/status.py +781 -0
  146. deepagents_code/tui/widgets/subagent_panel.py +969 -0
  147. deepagents_code/tui/widgets/theme_selector.py +401 -0
  148. deepagents_code/tui/widgets/thread_selector.py +2564 -0
  149. deepagents_code/tui/widgets/tool_renderers.py +190 -0
  150. deepagents_code/tui/widgets/tool_widgets.py +304 -0
  151. deepagents_code/tui/widgets/update_available.py +311 -0
  152. deepagents_code/tui/widgets/update_confirm.py +184 -0
  153. deepagents_code/tui/widgets/update_progress.py +327 -0
  154. deepagents_code/tui/widgets/welcome.py +508 -0
  155. deepagents_code/turn_end_summary.py +522 -0
  156. deepagents_code/ui.py +858 -0
  157. deepagents_code/unicode_security.py +563 -0
  158. deepagents_code/update_check.py +3124 -0
  159. zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
  160. zjcode-0.0.1.dist-info/METADATA +220 -0
  161. zjcode-0.0.1.dist-info/RECORD +163 -0
  162. zjcode-0.0.1.dist-info/WHEEL +4 -0
  163. zjcode-0.0.1.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,42 @@
1
+ """Deep Agents Code - Interactive AI coding assistant."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING
7
+
8
+ from deepagents_code._debug import configure_debug_logging
9
+ from deepagents_code._debug_buffer import install_log_buffer
10
+ from deepagents_code._version import __version__
11
+
12
+ if TYPE_CHECKING:
13
+ from collections.abc import Callable
14
+
15
+ install_log_buffer(logging.getLogger(__name__)) # noqa: RUF067 # attach the always-on tail first so warnings from configure_debug_logging are captured
16
+ configure_debug_logging(logging.getLogger(__name__)) # noqa: RUF067 # package logger must be configured before child modules emit logs; sets the final level over the buffer's INFO floor
17
+
18
+ __all__ = [
19
+ "__version__",
20
+ "cli_main", # noqa: F822 # resolved lazily by __getattr__
21
+ ]
22
+
23
+
24
+ def __getattr__(name: str) -> Callable[[], None]:
25
+ """Lazy import for `cli_main` to avoid loading `main.py` at package import.
26
+
27
+ `main.py` pulls in `argparse`, signal handling, and other startup machinery
28
+ that isn't needed when submodules like `config` or `widgets` are
29
+ imported directly.
30
+
31
+ Returns:
32
+ The requested callable.
33
+
34
+ Raises:
35
+ AttributeError: If *name* is not a lazily-provided attribute.
36
+ """
37
+ if name == "cli_main":
38
+ from deepagents_code.main import cli_main
39
+
40
+ return cli_main
41
+ msg = f"module {__name__!r} has no attribute {name!r}"
42
+ raise AttributeError(msg)
@@ -0,0 +1,6 @@
1
+ """Allow running the CLI as: `python -m deepagents_code`."""
2
+
3
+ from deepagents_code.main import cli_main
4
+
5
+ if __name__ == "__main__":
6
+ cli_main()
@@ -0,0 +1,90 @@
1
+ """Lightweight types for the ask-user interrupt protocol.
2
+
3
+ Extracted from `ask_user` so `textual_adapter` can import `AskUserRequest` at
4
+ module level — and `app` can reference the types at type-check time — without
5
+ pulling in the langchain middleware stack.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Annotated, Literal, NotRequired
11
+
12
+ from pydantic import Field
13
+ from typing_extensions import TypedDict
14
+
15
+
16
+ class Choice(TypedDict):
17
+ """A single choice option for a multiple choice question."""
18
+
19
+ value: Annotated[str, Field(description="The display label for this choice.")]
20
+
21
+
22
+ class Question(TypedDict):
23
+ """A question to ask the user."""
24
+
25
+ question: Annotated[str, Field(description="The question text to display.")]
26
+
27
+ type: Annotated[
28
+ Literal["text", "multiple_choice"],
29
+ Field(
30
+ description=(
31
+ "Question type. 'text' for free-form input, 'multiple_choice' for "
32
+ "predefined options."
33
+ )
34
+ ),
35
+ ]
36
+
37
+ choices: NotRequired[
38
+ Annotated[
39
+ list[Choice],
40
+ Field(
41
+ description=(
42
+ "Options for multiple_choice questions. An 'Other' free-form "
43
+ "option is always appended automatically."
44
+ )
45
+ ),
46
+ ]
47
+ ]
48
+
49
+ required: NotRequired[
50
+ Annotated[
51
+ bool,
52
+ Field(
53
+ description="Whether the user must answer. Defaults to true if omitted."
54
+ ),
55
+ ]
56
+ ]
57
+
58
+
59
+ class AskUserRequest(TypedDict):
60
+ """Request payload sent via interrupt when asking the user questions."""
61
+
62
+ type: Literal["ask_user"]
63
+ """Discriminator tag, always `'ask_user'`."""
64
+
65
+ questions: list[Question]
66
+ """Questions to present to the user."""
67
+
68
+ tool_call_id: str
69
+ """ID of the originating tool call, used to route the response back."""
70
+
71
+
72
+ class AskUserAnswered(TypedDict):
73
+ """Widget result when the user submits answers."""
74
+
75
+ type: Literal["answered"]
76
+ """Discriminator tag, always `'answered'`."""
77
+
78
+ answers: list[str]
79
+ """User-provided answers, one per question."""
80
+
81
+
82
+ class AskUserCancelled(TypedDict):
83
+ """Widget result when the user cancels the prompt."""
84
+
85
+ type: Literal["cancelled"]
86
+ """Discriminator tag, always `'cancelled'`."""
87
+
88
+
89
+ AskUserWidgetResult = AskUserAnswered | AskUserCancelled
90
+ """Discriminated union for the ask_user widget Future result."""
@@ -0,0 +1,96 @@
1
+ """Lightweight runtime context types for the CLI agent graph.
2
+
3
+ Carries per-run overrides (model swap/params, approval mode) passed via
4
+ `context=`. Extracted from `configurable_model` so hot-path modules (`app`,
5
+ `textual_adapter`) can import `CLIContext` without pulling in the langchain
6
+ middleware stack.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass, field
12
+ from typing import Any, TypedDict
13
+
14
+
15
+ @dataclass
16
+ class CLIContextSchema:
17
+ """Declared `context_schema` for the agent graph.
18
+
19
+ Registered via `context_schema=` when the graph is built, so LangGraph
20
+ coerces each run's `context=` payload into this dataclass — in-process,
21
+ `runtime.context` is a `CLIContextSchema` instance.
22
+
23
+ It exists alongside `CLIContext` (below) because the payload is shaped
24
+ differently on each side of the API boundary: in-process it is coerced to
25
+ this dataclass, but over the LangGraph API server (RemoteGraph) it is
26
+ serialized to JSON and arrives as a plain dict. Consumers
27
+ (`configurable_model._get_context`, `_should_interrupt_tool_call`)
28
+ therefore accept both shapes. `CLIContext` is the client-facing builder for
29
+ constructing that payload.
30
+
31
+ Fields mirror `CLIContext`; see its per-field docstrings for semantics.
32
+ """
33
+
34
+ model: str | None = None
35
+
36
+ model_params: dict[str, Any] = field(default_factory=dict)
37
+
38
+ auto_approve: bool = False
39
+
40
+ approval_mode_key: str | None = None
41
+
42
+ thread_id: str | None = None
43
+
44
+ blocked_goal_retry_context: str | None = None
45
+
46
+
47
+ class CLIContext(TypedDict, total=False):
48
+ """Client-facing builder for the per-run graph context payload.
49
+
50
+ Callers populate this and pass it via `context=` to `astream`/`ainvoke`.
51
+ `ConfigurableModelMiddleware` and the `interrupt_on` `when` predicate read
52
+ it from `request.runtime.context`. In-process LangGraph coerces it into
53
+ `CLIContextSchema` (the registered `context_schema`); over the API it stays
54
+ a plain dict — which is why consumers handle both shapes.
55
+ """
56
+
57
+ model: str | None
58
+ """Model spec to swap at runtime (e.g. `'provider:model'`)."""
59
+
60
+ model_params: dict[str, Any]
61
+ """Invocation params (e.g. `temperature`, `max_tokens`) to merge
62
+ into `model_settings`."""
63
+
64
+ auto_approve: bool
65
+ """Whether gated tool calls should skip the human-approval interrupt.
66
+
67
+ Sourced from the client session (not graph state) so the model cannot
68
+ self-approve by writing state. The `interrupt_on` `when` predicate reads
69
+ this to suppress interrupts at the source when "approve always" is on,
70
+ avoiding the interrupt-then-auto-resolve round-trip.
71
+ """
72
+
73
+ approval_mode_key: str | None
74
+ """Store key for the live approval-mode control record.
75
+
76
+ The TUI updates this record when the user toggles approval mode mid-run.
77
+ The server-side interrupt predicate reads it from the LangGraph Store on
78
+ each gated tool call so auto-to-manual changes can take effect before the
79
+ current stream returns.
80
+ """
81
+
82
+ thread_id: str | None
83
+ """LangGraph thread ID for the active conversation.
84
+
85
+ Mirrors `config.configurable.thread_id` into runtime context for model-call
86
+ middleware that needs per-request session identity, including Fireworks
87
+ session-affinity headers.
88
+ """
89
+
90
+ blocked_goal_retry_context: str | None
91
+ """One-turn model context for retrying a previously blocked goal.
92
+
93
+ This is intentionally carried in runtime context instead of the user
94
+ message so it is not parsed as a file mention or checkpointed as human
95
+ input.
96
+ """
@@ -0,0 +1,41 @@
1
+ """Lightweight shared constants for the app.
2
+
3
+ This module is intentionally dependency-free (no third-party imports, no
4
+ sibling-module imports) so any other module — including the startup-critical
5
+ `main.py` and the heavy `agent.py` — can import from it without triggering a
6
+ chain of expensive imports.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Final
12
+
13
+ DEFAULT_AGENT_NAME: Final[str] = "agent"
14
+ """Default agent / assistant identifier when no `-a` flag is given."""
15
+
16
+ FIREWORKS_PROVIDER_ID_PREFIX: Final[str] = "accounts/fireworks/"
17
+ """Prefix used to infer Fireworks from fully-qualified IDs."""
18
+
19
+ FIREWORKS_MODEL_ID_PREFIXES: Final[tuple[str, ...]] = (
20
+ "accounts/fireworks/models/",
21
+ "accounts/fireworks/routers/",
22
+ )
23
+ """Model and router ID prefixes used for stripping and classification."""
24
+
25
+ MCP_REENABLED_PENDING_ERROR: Final[str] = "Re-enabled — press Ctrl+R to load."
26
+ """User-facing reconnect guidance shown for an MCP server that was optimistically
27
+ re-enabled but whose agent has not yet reconnected.
28
+
29
+ Set as `MCPServerInfo.error` by `app._apply_optimistic_disabled_state` (alongside
30
+ `pending_reconnect=True`, which is what `/tools` actually keys off). Named here
31
+ so the producer and the tests asserting the message share one literal.
32
+ """
33
+
34
+ SYSTEM_MESSAGE_PREFIX: Final[str] = "[SYSTEM]"
35
+ """Prefix for synthetic human messages (e.g. interrupt cancellation notices).
36
+
37
+ Such messages are written to the `messages` channel for the agent's benefit on
38
+ resume but are not user-authored, so they are filtered out of both the rendered
39
+ transcript and a thread's initial prompt. Shared here so the single producer
40
+ (`textual_adapter`) and its consumers (`app`, `sessions`) agree on one literal.
41
+ """
@@ -0,0 +1,148 @@
1
+ """Shared debug-logging configuration for runtime and file-based tracing.
2
+
3
+ When the `DEEPAGENTS_CODE_DEBUG` environment variable is set, modules that handle
4
+ streaming or remote communication can enable detailed file-based logging. This
5
+ helper centralizes the setup so the env-var names, file path, log level, and
6
+ format are defined in one place.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ import os
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ from deepagents_code._env_vars import (
17
+ DEBUG,
18
+ DEBUG_FILE,
19
+ DEFAULT_DEBUG_FILE,
20
+ LOG_LEVEL,
21
+ is_env_truthy,
22
+ )
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ _DEBUG_HANDLER_ATTR = "_deepagents_code_debug_handler"
27
+ LOG_LEVELS = {
28
+ "DEBUG": logging.DEBUG,
29
+ "INFO": logging.INFO,
30
+ "WARNING": logging.WARNING,
31
+ "ERROR": logging.ERROR,
32
+ "CRITICAL": logging.CRITICAL,
33
+ }
34
+ """Canonical level-name to `logging` level mapping.
35
+
36
+ The single source of truth for level names and their numeric values, shared with
37
+ the Debug Console's level filter so severity ordering is never re-derived from
38
+ hardcoded integers.
39
+ """
40
+
41
+
42
+ def resolve_log_level(*, debug_enabled: bool | None = None) -> int:
43
+ """Resolve the configured runtime logging level.
44
+
45
+ Args:
46
+ debug_enabled: Whether `DEEPAGENTS_CODE_DEBUG` is truthy. When omitted,
47
+ the current environment is checked.
48
+
49
+ Returns:
50
+ A standard `logging` level integer. Defaults to `DEBUG` when debug file
51
+ logging is enabled and `INFO` otherwise.
52
+ """
53
+ if debug_enabled is None:
54
+ debug_enabled = is_env_truthy(DEBUG)
55
+ fallback = logging.DEBUG if debug_enabled else logging.INFO
56
+ raw = os.environ.get(LOG_LEVEL)
57
+ if raw is None or not raw.strip():
58
+ return fallback
59
+ level = LOG_LEVELS.get(raw.strip().upper())
60
+ if level is not None:
61
+ return level
62
+ valid = ", ".join(LOG_LEVELS)
63
+ message = f"ignoring invalid {LOG_LEVEL}={raw!r}; expected one of {valid}"
64
+ # stderr for headless / pre-TUI visibility; the logger so it also lands in
65
+ # the always-on in-memory buffer and surfaces in the Debug Console (the
66
+ # buffer is installed before this runs; see __init__.py).
67
+ print(f"Warning: {message}", file=sys.stderr) # noqa: T201
68
+ logger.warning("%s", message)
69
+ return fallback
70
+
71
+
72
+ def configure_debug_logging(target: logging.Logger) -> None:
73
+ """Configure runtime log level and optional file logging for *target*.
74
+
75
+ Intended to be called once on the `deepagents_code` package logger; child
76
+ module loggers reach the same handlers via propagation, so individual modules
77
+ do not configure logging themselves.
78
+
79
+ `DEEPAGENTS_CODE_LOG_LEVEL` controls the package logger level independently of
80
+ file logging. If it is unset, `DEEPAGENTS_CODE_DEBUG=1` defaults to `DEBUG`;
81
+ otherwise the runtime level defaults to `INFO`.
82
+
83
+ When `DEEPAGENTS_CODE_DEBUG` is truthy, a file handler is attached. The log
84
+ file defaults to `DEFAULT_DEBUG_FILE` but can be overridden with
85
+ `DEEPAGENTS_CODE_DEBUG_FILE`. The handler appends (`mode='a'`) so logs are
86
+ preserved across separate process runs. Calling this again with the same
87
+ resolved path does not stack duplicate handlers: the existing tagged handler
88
+ is reused and its level re-applied. If the resolved path changes, the stale
89
+ handler is closed and replaced.
90
+
91
+ Args:
92
+ target: Logger to configure.
93
+ """
94
+ debug_enabled = is_env_truthy(DEBUG)
95
+ level = resolve_log_level(debug_enabled=debug_enabled)
96
+ target.setLevel(level)
97
+
98
+ if not debug_enabled:
99
+ return
100
+
101
+ debug_path = Path(os.environ.get(DEBUG_FILE, DEFAULT_DEBUG_FILE))
102
+ for existing in list(target.handlers):
103
+ if not (
104
+ isinstance(existing, logging.FileHandler)
105
+ and getattr(existing, _DEBUG_HANDLER_ATTR, False)
106
+ ):
107
+ continue
108
+ if Path(existing.baseFilename) == debug_path:
109
+ existing.setLevel(level)
110
+ return
111
+ # The debug path changed; drop the stale handler before re-attaching so
112
+ # we don't leak its file descriptor or fan logs out to two files.
113
+ target.removeHandler(existing)
114
+ existing.close()
115
+
116
+ try:
117
+ handler = logging.FileHandler(str(debug_path), mode="a")
118
+ except OSError as exc:
119
+ message = f"could not open debug log file {debug_path}: {exc}"
120
+ # stderr for headless / pre-TUI visibility; the logger so it also lands
121
+ # in the always-on in-memory buffer and surfaces in the Debug Console.
122
+ print(f"Warning: {message}", file=sys.stderr) # noqa: T201
123
+ logger.warning("%s", message)
124
+ return
125
+ setattr(handler, _DEBUG_HANDLER_ATTR, True)
126
+ handler.setLevel(level)
127
+ handler.setFormatter(logging.Formatter("%(asctime)s %(name)s %(message)s"))
128
+ target.addHandler(handler)
129
+
130
+
131
+ def installed_debug_log_path() -> Path | None:
132
+ """Return the path of the active debug log file, or `None` if not logging.
133
+
134
+ Reflects the file handler actually attached by `configure_debug_logging`,
135
+ not the current `DEEPAGENTS_CODE_DEBUG` env value. The two diverge when the
136
+ variable is set after import — e.g. via a project/global `.env` loaded during
137
+ settings bootstrap — in which case the variable reads truthy but no handler
138
+ was installed and no log file exists. Callers that surface "full error in
139
+ <path>" hints must use this rather than the env var to avoid pointing users
140
+ at a file that was never created.
141
+ """
142
+ package_logger = logging.getLogger(__package__ or "deepagents_code")
143
+ for handler in package_logger.handlers:
144
+ if isinstance(handler, logging.FileHandler) and getattr(
145
+ handler, _DEBUG_HANDLER_ATTR, False
146
+ ):
147
+ return Path(handler.baseFilename)
148
+ return None
@@ -0,0 +1,204 @@
1
+ r"""In-memory ring buffer of recent log records for the in-app Debug Console.
2
+
3
+ A lightweight `logging.Handler` keeps the most recent structured log records in a
4
+ bounded `deque` so the Debug Console (`Ctrl+\`) can show a live tail without
5
+ requiring the opt-in file logging from `_debug.configure_debug_logging`. The
6
+ handler is installed once on the `deepagents_code` package logger (see
7
+ `__init__.py`); child module loggers reach it via propagation.
8
+
9
+ Installation is negligible, and each emitted record only appends a structured
10
+ record to a bounded `deque`, so the buffer is cheap enough to keep always on.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import itertools
16
+ import logging
17
+ import os
18
+ from collections import deque
19
+ from dataclasses import dataclass
20
+
21
+ from deepagents_code._env_vars import LOG_LEVEL
22
+
23
+ DEFAULT_CAPACITY = 1000
24
+ """Maximum number of records retained in the ring buffer."""
25
+
26
+ _DATE_FORMAT = "%H:%M:%S"
27
+ _FORMATTER = logging.Formatter(datefmt=_DATE_FORMAT)
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class InMemoryLogRecord:
32
+ """Structured log record retained by the in-memory debug buffer."""
33
+
34
+ timestamp: str
35
+ level: str
36
+ levelno: int
37
+ logger: str
38
+ message: str
39
+
40
+ @property
41
+ def plain_line(self) -> str:
42
+ """The record in the legacy plain-text debug-console format."""
43
+ return f"{self.timestamp} {self.level} {self.logger} {self.message}"
44
+
45
+
46
+ class InMemoryLogBuffer(logging.Handler):
47
+ """Logging handler retaining the most recent structured records in memory."""
48
+
49
+ def __init__(self, capacity: int = DEFAULT_CAPACITY) -> None:
50
+ """Create the handler with a bounded backing `deque`.
51
+
52
+ Args:
53
+ capacity: Maximum records to retain; oldest are dropped first.
54
+
55
+ Raises:
56
+ ValueError: If *capacity* is less than 1. A zero-capacity buffer
57
+ would silently discard every record while `total_emitted` kept
58
+ climbing, so it is rejected at the boundary.
59
+ """
60
+ if capacity < 1:
61
+ msg = f"capacity must be >= 1, got {capacity}"
62
+ raise ValueError(msg)
63
+ super().__init__()
64
+ self._records: deque[InMemoryLogRecord] = deque(maxlen=capacity)
65
+ self._total = 0
66
+
67
+ def emit(self, record: logging.LogRecord) -> None:
68
+ """Append the structured *record* to the ring buffer."""
69
+ self.acquire()
70
+ try:
71
+ self._records.append(self._make_record(record))
72
+ self._total += 1
73
+ except Exception: # noqa: BLE001 # never let logging crash the app
74
+ self.handleError(record)
75
+ finally:
76
+ self.release()
77
+
78
+ @property
79
+ def total_emitted(self) -> int:
80
+ """Total records ever emitted (monotonic; survives buffer eviction)."""
81
+ self.acquire()
82
+ try:
83
+ return self._total
84
+ finally:
85
+ self.release()
86
+
87
+ def snapshot_records_since(self, index: int) -> tuple[list[InMemoryLogRecord], int]:
88
+ """Return retained structured records and the next absolute index.
89
+
90
+ Absolute indices are stable even as old records are evicted, so callers
91
+ can poll incrementally without re-reading records they already consumed.
92
+ Returning the records and the resume index together under the handler
93
+ lock prevents a concurrent append from being skipped between separate
94
+ reads.
95
+
96
+ Args:
97
+ index: Absolute emission index to start from.
98
+
99
+ Returns:
100
+ A tuple of the structured records from *index* onward that are still
101
+ retained, and the current total emitted count to resume from.
102
+ """
103
+ self.acquire()
104
+ try:
105
+ return self._records_since_unlocked(index), self._total
106
+ finally:
107
+ self.release()
108
+
109
+ def _records_since_unlocked(self, index: int) -> list[InMemoryLogRecord]:
110
+ """Return retained records from *index* while the handler lock is held."""
111
+ start_abs = self._total - len(self._records)
112
+ offset = index - start_abs
113
+ if offset <= 0:
114
+ return list(self._records)
115
+ if offset >= len(self._records):
116
+ return []
117
+ # islice avoids materializing the full deque just to slice off the tail.
118
+ return list(itertools.islice(self._records, offset, None))
119
+
120
+ @staticmethod
121
+ def _make_record(record: logging.LogRecord) -> InMemoryLogRecord:
122
+ """Convert a standard logging record into a structured debug record.
123
+
124
+ Returns:
125
+ Structured record for display and filtering.
126
+ """
127
+ message = record.getMessage()
128
+ if record.exc_info:
129
+ if not record.exc_text:
130
+ record.exc_text = _FORMATTER.formatException(record.exc_info)
131
+ if record.exc_text:
132
+ message = f"{message}\n{record.exc_text}"
133
+ if record.stack_info:
134
+ message = f"{message}\n{_FORMATTER.formatStack(record.stack_info)}"
135
+ return InMemoryLogRecord(
136
+ timestamp=_FORMATTER.formatTime(record, _DATE_FORMAT),
137
+ level=record.levelname,
138
+ levelno=record.levelno,
139
+ logger=record.name,
140
+ message=message,
141
+ )
142
+
143
+
144
+ _buffer: InMemoryLogBuffer | None = None
145
+
146
+
147
+ def install_log_buffer(
148
+ target: logging.Logger, capacity: int = DEFAULT_CAPACITY
149
+ ) -> InMemoryLogBuffer:
150
+ """Attach the in-memory buffer handler to *target* (idempotent).
151
+
152
+ Lowers *target*'s level to at most `INFO` so the console shows a useful tail
153
+ even when `DEEPAGENTS_CODE_DEBUG` is off; never raises the level. In
154
+ `__init__.py` this runs *before* `configure_debug_logging`, which then sets
155
+ the final level (honoring `DEEPAGENTS_CODE_DEBUG` and
156
+ `DEEPAGENTS_CODE_LOG_LEVEL`) over this `INFO` floor — so any startup warnings
157
+ `configure_debug_logging` emits are captured by the already-installed buffer.
158
+ On a fresh `NOTSET` logger the `NOTSET` branch forces `INFO` without
159
+ consulting `DEEPAGENTS_CODE_LOG_LEVEL`; the `> INFO and no env` branch only
160
+ matters on reconfiguration (e.g. an `importlib.reload`), where it preserves
161
+ an explicit `DEEPAGENTS_CODE_LOG_LEVEL` rather than clobbering it with `INFO`.
162
+
163
+ Lowering the level does not spill log output onto the terminal: because this
164
+ handler is present in the propagation chain, `Logger.callHandlers` finds a
165
+ handler (`found > 0`) and Python's `lastResort` stderr handler is never
166
+ consulted. The exception is an embedding process that attaches its own
167
+ `INFO`-or-lower handler to the root logger, which would then see the
168
+ propagated records.
169
+
170
+ Note: this runs as an import-time side effect (see `__init__.py`), so every
171
+ `import deepagents_code` attaches the handler and may lower the package
172
+ logger's level to `INFO` for the lifetime of the process.
173
+
174
+ Args:
175
+ target: Logger to attach the buffer to (the package logger).
176
+ capacity: Maximum records to retain.
177
+
178
+ Returns:
179
+ The installed (or already-installed) buffer handler.
180
+ """
181
+ global _buffer # noqa: PLW0603 # module-level singleton accessor
182
+ for existing in target.handlers:
183
+ if isinstance(existing, InMemoryLogBuffer):
184
+ _buffer = existing
185
+ return existing
186
+
187
+ handler = InMemoryLogBuffer(capacity)
188
+ handler.setLevel(logging.DEBUG)
189
+ target.addHandler(handler)
190
+ if target.level == logging.NOTSET or (
191
+ target.level > logging.INFO and not os.environ.get(LOG_LEVEL)
192
+ ):
193
+ target.setLevel(logging.INFO)
194
+ _buffer = handler
195
+ return handler
196
+
197
+
198
+ def get_log_buffer() -> InMemoryLogBuffer | None:
199
+ """Return the installed buffer handler.
200
+
201
+ Returns:
202
+ The installed buffer handler, or `None` if not yet installed.
203
+ """
204
+ return _buffer