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.
- deepagents_code/__init__.py +42 -0
- deepagents_code/__main__.py +6 -0
- deepagents_code/_ask_user_types.py +90 -0
- deepagents_code/_cli_context.py +96 -0
- deepagents_code/_constants.py +41 -0
- deepagents_code/_debug.py +148 -0
- deepagents_code/_debug_buffer.py +204 -0
- deepagents_code/_env_vars.py +411 -0
- deepagents_code/_fake_models.py +66 -0
- deepagents_code/_git.py +521 -0
- deepagents_code/_paths.py +69 -0
- deepagents_code/_server_config.py +576 -0
- deepagents_code/_session_stats.py +235 -0
- deepagents_code/_startup_error.py +45 -0
- deepagents_code/_testing_models.py +305 -0
- deepagents_code/_textual_patches.py +420 -0
- deepagents_code/_tool_stream.py +694 -0
- deepagents_code/_version.py +46 -0
- deepagents_code/agent.py +1976 -0
- deepagents_code/app.py +19239 -0
- deepagents_code/app.tcss +438 -0
- deepagents_code/approval_mode.py +131 -0
- deepagents_code/ask_user.py +306 -0
- deepagents_code/auth_display.py +185 -0
- deepagents_code/auth_store.py +545 -0
- deepagents_code/built_in_skills/__init__.py +5 -0
- deepagents_code/built_in_skills/remember/SKILL.md +118 -0
- deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
- deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
- deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
- deepagents_code/client/__init__.py +1 -0
- deepagents_code/client/commands/__init__.py +1 -0
- deepagents_code/client/commands/auth.py +541 -0
- deepagents_code/client/commands/config.py +714 -0
- deepagents_code/client/commands/mcp.py +250 -0
- deepagents_code/client/commands/tools.py +416 -0
- deepagents_code/client/launch/__init__.py +1 -0
- deepagents_code/client/launch/server.py +978 -0
- deepagents_code/client/launch/server_manager.py +540 -0
- deepagents_code/client/non_interactive.py +1758 -0
- deepagents_code/client/remote_client.py +794 -0
- deepagents_code/clipboard.py +217 -0
- deepagents_code/command_registry.py +450 -0
- deepagents_code/config.py +4829 -0
- deepagents_code/config_manifest.py +1451 -0
- deepagents_code/configurable_model.py +577 -0
- deepagents_code/default_agent_prompt.md +12 -0
- deepagents_code/doctor.py +559 -0
- deepagents_code/editor.py +142 -0
- deepagents_code/event_bus.py +411 -0
- deepagents_code/extras_info.py +661 -0
- deepagents_code/file_ops.py +576 -0
- deepagents_code/formatting.py +135 -0
- deepagents_code/goal_rubric.py +497 -0
- deepagents_code/goal_tools.py +459 -0
- deepagents_code/hooks.py +348 -0
- deepagents_code/input.py +1041 -0
- deepagents_code/integrations/__init__.py +1 -0
- deepagents_code/integrations/openai_codex.py +551 -0
- deepagents_code/integrations/sandbox_config.py +198 -0
- deepagents_code/integrations/sandbox_factory.py +1124 -0
- deepagents_code/integrations/sandbox_provider.py +137 -0
- deepagents_code/integrations/sandbox_registry.py +350 -0
- deepagents_code/iterm_cursor_guide.py +176 -0
- deepagents_code/local_context.py +926 -0
- deepagents_code/main.py +3985 -0
- deepagents_code/managed_tools.py +642 -0
- deepagents_code/mcp_auth.py +1950 -0
- deepagents_code/mcp_config.py +176 -0
- deepagents_code/mcp_disabled.py +212 -0
- deepagents_code/mcp_login_service.py +281 -0
- deepagents_code/mcp_oauth_ui.py +199 -0
- deepagents_code/mcp_providers/__init__.py +23 -0
- deepagents_code/mcp_providers/_registry.py +39 -0
- deepagents_code/mcp_providers/base.py +133 -0
- deepagents_code/mcp_providers/github.py +102 -0
- deepagents_code/mcp_providers/slack.py +175 -0
- deepagents_code/mcp_tools.py +2427 -0
- deepagents_code/mcp_trust.py +207 -0
- deepagents_code/media_utils.py +826 -0
- deepagents_code/memory_guard.py +474 -0
- deepagents_code/model_config.py +4156 -0
- deepagents_code/notifications.py +247 -0
- deepagents_code/offload.py +402 -0
- deepagents_code/onboarding.py +223 -0
- deepagents_code/output.py +69 -0
- deepagents_code/paste_collapse.py +103 -0
- deepagents_code/project_utils.py +231 -0
- deepagents_code/py.typed +0 -0
- deepagents_code/reasoning_effort.py +641 -0
- deepagents_code/reliable_rubric.py +97 -0
- deepagents_code/resume_state.py +237 -0
- deepagents_code/server_graph.py +310 -0
- deepagents_code/session_end_summary.py +329 -0
- deepagents_code/sessions.py +1716 -0
- deepagents_code/skills/__init__.py +18 -0
- deepagents_code/skills/commands.py +1252 -0
- deepagents_code/skills/invocation.py +112 -0
- deepagents_code/skills/load.py +196 -0
- deepagents_code/skills/trust.py +547 -0
- deepagents_code/state_migration.py +136 -0
- deepagents_code/subagents.py +278 -0
- deepagents_code/system_prompt.md +204 -0
- deepagents_code/terminal_capabilities.py +115 -0
- deepagents_code/terminal_escape.py +287 -0
- deepagents_code/theme.py +891 -0
- deepagents_code/todo_list_prompt.md +12 -0
- deepagents_code/tool_catalog.py +509 -0
- deepagents_code/tool_display.py +367 -0
- deepagents_code/tools.py +516 -0
- deepagents_code/tui/__init__.py +1 -0
- deepagents_code/tui/textual_adapter.py +2553 -0
- deepagents_code/tui/widgets/__init__.py +9 -0
- deepagents_code/tui/widgets/_js_eval_display.py +139 -0
- deepagents_code/tui/widgets/_links.py +261 -0
- deepagents_code/tui/widgets/agent_selector.py +420 -0
- deepagents_code/tui/widgets/approval.py +602 -0
- deepagents_code/tui/widgets/ask_user.py +515 -0
- deepagents_code/tui/widgets/auth.py +1997 -0
- deepagents_code/tui/widgets/autocomplete.py +890 -0
- deepagents_code/tui/widgets/chat_input.py +3181 -0
- deepagents_code/tui/widgets/codex_auth.py +452 -0
- deepagents_code/tui/widgets/cwd_switch.py +242 -0
- deepagents_code/tui/widgets/debug_console.py +868 -0
- deepagents_code/tui/widgets/diff.py +252 -0
- deepagents_code/tui/widgets/effort_selector.py +189 -0
- deepagents_code/tui/widgets/goal_review.py +390 -0
- deepagents_code/tui/widgets/goal_status.py +50 -0
- deepagents_code/tui/widgets/history.py +194 -0
- deepagents_code/tui/widgets/install_confirm.py +248 -0
- deepagents_code/tui/widgets/launch_init.py +482 -0
- deepagents_code/tui/widgets/loading.py +227 -0
- deepagents_code/tui/widgets/mcp_login.py +539 -0
- deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
- deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
- deepagents_code/tui/widgets/message_store.py +999 -0
- deepagents_code/tui/widgets/messages.py +3846 -0
- deepagents_code/tui/widgets/model_selector.py +2343 -0
- deepagents_code/tui/widgets/notification_center.py +456 -0
- deepagents_code/tui/widgets/notification_detail.py +257 -0
- deepagents_code/tui/widgets/notification_settings.py +170 -0
- deepagents_code/tui/widgets/restart_prompt.py +158 -0
- deepagents_code/tui/widgets/skill_trust.py +131 -0
- deepagents_code/tui/widgets/startup_tip.py +91 -0
- deepagents_code/tui/widgets/status.py +781 -0
- deepagents_code/tui/widgets/subagent_panel.py +969 -0
- deepagents_code/tui/widgets/theme_selector.py +401 -0
- deepagents_code/tui/widgets/thread_selector.py +2564 -0
- deepagents_code/tui/widgets/tool_renderers.py +190 -0
- deepagents_code/tui/widgets/tool_widgets.py +304 -0
- deepagents_code/tui/widgets/update_available.py +311 -0
- deepagents_code/tui/widgets/update_confirm.py +184 -0
- deepagents_code/tui/widgets/update_progress.py +327 -0
- deepagents_code/tui/widgets/welcome.py +508 -0
- deepagents_code/turn_end_summary.py +522 -0
- deepagents_code/ui.py +858 -0
- deepagents_code/unicode_security.py +563 -0
- deepagents_code/update_check.py +3124 -0
- zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
- zjcode-0.0.1.dist-info/METADATA +220 -0
- zjcode-0.0.1.dist-info/RECORD +163 -0
- zjcode-0.0.1.dist-info/WHEEL +4 -0
- 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,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
|