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,12 @@
|
|
|
1
|
+
### Todo List Management
|
|
2
|
+
|
|
3
|
+
When using the write_todos tool:
|
|
4
|
+
|
|
5
|
+
1. Use todos for any task with 2+ steps — they give the user visibility
|
|
6
|
+
2. Mark tasks `in_progress` before starting, `completed` immediately after
|
|
7
|
+
3. Don't batch completions — mark each item done as you finish it
|
|
8
|
+
4. If a task reveals sub-tasks, add them right away
|
|
9
|
+
5. For simple 1-step tasks, just do them directly
|
|
10
|
+
{todo_guidance}
|
|
11
|
+
|
|
12
|
+
The todo list is a planning tool - use it judiciously to avoid overwhelming the user with excessive task tracking.
|
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
"""Enumerate the tools available to the agent.
|
|
2
|
+
|
|
3
|
+
Backs two entry points: the `dcode tools list` CLI command (`_run_tools_list`)
|
|
4
|
+
and the interactive `/tools` slash command (`app._handle_tools_command`).
|
|
5
|
+
|
|
6
|
+
The tool set is read from the *real* tool objects the agent binds rather than a
|
|
7
|
+
hand-maintained catalog, so names and descriptions never drift from what the
|
|
8
|
+
model actually sees. Built-in tools are collected by compiling the agent with a
|
|
9
|
+
throwaway offline chat model (no credentials, no network) and reading the bound
|
|
10
|
+
tool node; MCP tools are discovered via the same path the app and server use.
|
|
11
|
+
|
|
12
|
+
The collection functions here lazily import the heavy agent stack (agent
|
|
13
|
+
compilation, MCP discovery) inside their bodies. Only the fake-model base is
|
|
14
|
+
imported at module top, so importing this module is cheap relative to the agent
|
|
15
|
+
stack — and this module is itself imported lazily by both entry points
|
|
16
|
+
(`_run_tools_list` and `_handle_tools_command`), never on the startup hot path.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import asyncio
|
|
22
|
+
import logging
|
|
23
|
+
from collections.abc import Mapping
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import TYPE_CHECKING, Any, Literal, cast
|
|
27
|
+
|
|
28
|
+
from deepagents_code._fake_models import _ToolBindingFakeModel
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from collections.abc import Sequence
|
|
32
|
+
|
|
33
|
+
from langgraph.prebuilt.tool_node import ToolNode
|
|
34
|
+
|
|
35
|
+
from deepagents_code.mcp_tools import MCPServerInfo, MCPServerStatus
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
ToolSource = Literal["built-in", "mcp"]
|
|
40
|
+
"""Stable source token identifying where a tool group comes from.
|
|
41
|
+
|
|
42
|
+
Emitted verbatim in the `--json` output, so it is a public contract; keep it a
|
|
43
|
+
`Literal` of stable tokens (not a bare `str`), following the same convention as
|
|
44
|
+
`mcp_tools.MCPServerStatus`.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
BUILT_IN_GROUP = "Built-in"
|
|
48
|
+
"""Display label for the group of tools bundled with `deepagents-code`."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class ToolEntry:
|
|
53
|
+
"""A single tool's display metadata."""
|
|
54
|
+
|
|
55
|
+
name: str
|
|
56
|
+
"""Tool name as bound on the agent (e.g. `read_file`)."""
|
|
57
|
+
|
|
58
|
+
description: str
|
|
59
|
+
"""First non-empty line of the tool's description, whitespace-collapsed."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True, slots=True)
|
|
63
|
+
class ToolGroup:
|
|
64
|
+
"""A named group of tools sharing a source."""
|
|
65
|
+
|
|
66
|
+
label: str
|
|
67
|
+
"""Group heading (`Built-in`, or the MCP server name)."""
|
|
68
|
+
|
|
69
|
+
source: ToolSource
|
|
70
|
+
"""Stable source token: `built-in` or `mcp`."""
|
|
71
|
+
|
|
72
|
+
tools: tuple[ToolEntry, ...]
|
|
73
|
+
"""Tools in this group, in bind order."""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True, slots=True)
|
|
77
|
+
class UnavailableServer:
|
|
78
|
+
"""An MCP server that was discovered but currently exposes no tools."""
|
|
79
|
+
|
|
80
|
+
name: str
|
|
81
|
+
"""Server name from the MCP configuration."""
|
|
82
|
+
|
|
83
|
+
status: MCPServerStatus
|
|
84
|
+
"""Load status token — any non-`ok` `mcp_tools.MCPServerStatus`.
|
|
85
|
+
|
|
86
|
+
Reuses `MCPServerStatus` (rather than a bare `str`) so the same closed value
|
|
87
|
+
set governs this field and its `--json` output, and so the token list has a
|
|
88
|
+
single source of truth in `mcp_tools`.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
detail: str
|
|
92
|
+
"""Human-readable reason from discovery, or `""` when none was given.
|
|
93
|
+
|
|
94
|
+
For config-load failures this is discovery's own reason string, which may
|
|
95
|
+
include the local config file path (e.g. `~/.deepagents/mcp.json: ...`) —
|
|
96
|
+
the same text the interactive `/mcp` viewer shows. See `collect_mcp_catalog`.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __post_init__(self) -> None:
|
|
100
|
+
"""Enforce that an unavailable server is never `ok`.
|
|
101
|
+
|
|
102
|
+
An `ok` server exposes tools and belongs in a `ToolGroup`, never here;
|
|
103
|
+
rejecting it at construction keeps the documented non-`ok` invariant
|
|
104
|
+
from being silently violated by a future producer.
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
ValueError: If `status` is `"ok"`.
|
|
108
|
+
"""
|
|
109
|
+
if self.status == "ok":
|
|
110
|
+
msg = "UnavailableServer.status must be a non-'ok' MCPServerStatus"
|
|
111
|
+
raise ValueError(msg)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True, slots=True)
|
|
115
|
+
class ToolCatalog:
|
|
116
|
+
"""Everything `dcode tools list` needs to render, in display order."""
|
|
117
|
+
|
|
118
|
+
groups: tuple[ToolGroup, ...]
|
|
119
|
+
"""Built-in group first, then one group per MCP server that exposes tools."""
|
|
120
|
+
|
|
121
|
+
unavailable: tuple[UnavailableServer, ...] = ()
|
|
122
|
+
"""MCP servers discovered with no tools (errored, needing login, or disabled).
|
|
123
|
+
|
|
124
|
+
Surfaced rather than dropped so a user debugging a missing tool can see why
|
|
125
|
+
it is absent.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
mcp_error: str | None = None
|
|
129
|
+
"""Generic notice set when MCP discovery itself failed; `None` on success.
|
|
130
|
+
|
|
131
|
+
Raw exception detail is logged at debug level, never embedded here, so no
|
|
132
|
+
file paths or stack traces leak into CLI/JSON output.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def unavailable_server_display(server: UnavailableServer) -> tuple[str, str]:
|
|
137
|
+
"""Return the `(status_label, detail)` display pair for an unavailable server.
|
|
138
|
+
|
|
139
|
+
Shared by the CLI (`client.commands.tools._print_unavailable_servers`) and
|
|
140
|
+
TUI (`app._render_tool_catalog`) renderers so both describe a server the same
|
|
141
|
+
way. A disabled server shows its reconnect guidance if present, else the
|
|
142
|
+
generic "disabled by user", with no separate detail; other statuses show the
|
|
143
|
+
status token plus discovery's reason string when present.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
server: A server that loaded with no usable tools.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
`(status_label, detail)`: the primary status text and any secondary
|
|
150
|
+
detail (`""` when none). Each renderer lays these out itself, e.g. as
|
|
151
|
+
`status_label: detail`.
|
|
152
|
+
"""
|
|
153
|
+
if server.status == "disabled":
|
|
154
|
+
return (server.detail or "disabled by user", "")
|
|
155
|
+
return (server.status, server.detail)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class _CatalogModel(_ToolBindingFakeModel):
|
|
159
|
+
"""Offline placeholder model used only to compile the agent for enumeration.
|
|
160
|
+
|
|
161
|
+
Compiling the agent binds every tool but never calls the model, so this
|
|
162
|
+
never issues a request — enumeration only reads the bound tool node. It
|
|
163
|
+
exists so tool enumeration works without credentials or network access.
|
|
164
|
+
Inherits the `bind_tools` passthrough and minimal `profile` the agent
|
|
165
|
+
runtime reads during setup from `_ToolBindingFakeModel`.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
model: str = "catalog"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _first_line(text: str | None) -> str:
|
|
172
|
+
"""Return the first non-empty line of `text`, whitespace-collapsed."""
|
|
173
|
+
if not text:
|
|
174
|
+
return ""
|
|
175
|
+
for line in text.splitlines():
|
|
176
|
+
stripped = line.strip()
|
|
177
|
+
if stripped:
|
|
178
|
+
return " ".join(stripped.split())
|
|
179
|
+
return ""
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def collect_built_in_tools(
|
|
183
|
+
*, assistant_id: str = "agent", enable_interpreter: bool = False
|
|
184
|
+
) -> list[ToolEntry]:
|
|
185
|
+
"""Enumerate the built-in tools the agent binds by default.
|
|
186
|
+
|
|
187
|
+
Compiles the agent with an offline placeholder model and reads the bound
|
|
188
|
+
tool node. Memory and skills are disabled because they contribute no tools
|
|
189
|
+
(they only augment the system prompt). The selected assistant id is still
|
|
190
|
+
forwarded so agent-specific subagents are loaded from the same directory the
|
|
191
|
+
normal launch path uses. The custom CLI tools are included the same way
|
|
192
|
+
`server_graph._build_tools` adds them, so `web_search` appears only when
|
|
193
|
+
Tavily is configured.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
assistant_id: Resolved dcode agent identifier to compile.
|
|
197
|
+
enable_interpreter: Wire the JS interpreter middleware so `js_eval`
|
|
198
|
+
appears when the default agent would bind it. Callers should pass
|
|
199
|
+
the resolved runtime setting (see `_resolve_enable_interpreter`) so
|
|
200
|
+
the list matches the tools the agent actually binds.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Built-in tools in bind order.
|
|
204
|
+
|
|
205
|
+
Raises:
|
|
206
|
+
RuntimeError: If the compiled graph does not expose its bound tools.
|
|
207
|
+
"""
|
|
208
|
+
from deepagents_code.agent import create_cli_agent
|
|
209
|
+
from deepagents_code.config import settings
|
|
210
|
+
from deepagents_code.tools import fetch_url, get_current_thread_id, web_search
|
|
211
|
+
|
|
212
|
+
# Keep in sync with `server_graph._build_tools`: web_search is bound only
|
|
213
|
+
# when Tavily is configured, so it appears here only under the same gate.
|
|
214
|
+
custom_tools: list[Any] = [fetch_url, get_current_thread_id]
|
|
215
|
+
if settings.has_tavily:
|
|
216
|
+
custom_tools.append(web_search)
|
|
217
|
+
|
|
218
|
+
agent, _backend = create_cli_agent(
|
|
219
|
+
_CatalogModel(),
|
|
220
|
+
assistant_id=assistant_id,
|
|
221
|
+
tools=custom_tools,
|
|
222
|
+
enable_memory=False,
|
|
223
|
+
enable_skills=False,
|
|
224
|
+
enable_shell=True,
|
|
225
|
+
enable_interpreter=enable_interpreter,
|
|
226
|
+
)
|
|
227
|
+
tools = collect_tools_from_agent(agent)
|
|
228
|
+
if tools is None:
|
|
229
|
+
msg = "Compiled agent does not expose a LangGraph tool node"
|
|
230
|
+
raise RuntimeError(msg)
|
|
231
|
+
return tools
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def collect_tools_from_agent(agent: object) -> list[ToolEntry] | None:
|
|
235
|
+
"""Read tools from a local compiled agent when its graph is inspectable.
|
|
236
|
+
|
|
237
|
+
LangGraph does not expose a public tool-enumeration API, so this reaches
|
|
238
|
+
through the compiled graph's conventional `nodes["tools"].bound` shape.
|
|
239
|
+
Returning `None` distinguishes an uninspectable graph (a remote agent, or a
|
|
240
|
+
local graph whose internals no longer match that convention) from a local
|
|
241
|
+
graph that validly binds zero tools (`[]`).
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
agent: Active local or remote agent object.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Bound tools in graph order; `[]` for an inspectable local graph with no
|
|
248
|
+
tools; or `None` when the agent cannot be inspected locally.
|
|
249
|
+
"""
|
|
250
|
+
nodes = getattr(agent, "nodes", None)
|
|
251
|
+
if not isinstance(nodes, Mapping):
|
|
252
|
+
# No conventional node map: a remote agent or a non-graph object. Expected
|
|
253
|
+
# for remote agents, so debug rather than warning.
|
|
254
|
+
logger.debug("Agent %r has no inspectable node map", type(agent))
|
|
255
|
+
return None
|
|
256
|
+
if "tools" not in nodes:
|
|
257
|
+
# LangChain omits the tool node when an otherwise valid local agent
|
|
258
|
+
# binds no tools. The graph is still inspectable; its tool set is empty.
|
|
259
|
+
return []
|
|
260
|
+
node = nodes.get("tools")
|
|
261
|
+
tool_node = cast("ToolNode | None", getattr(node, "bound", None))
|
|
262
|
+
tools_by_name = getattr(tool_node, "tools_by_name", None)
|
|
263
|
+
if not isinstance(tools_by_name, Mapping):
|
|
264
|
+
# A "tools" node exists but does not expose the expected
|
|
265
|
+
# `bound.tools_by_name` mapping — a LangGraph internal-shape change, not
|
|
266
|
+
# a remote agent. Warn so this drift is visible in logs even though the
|
|
267
|
+
# user-facing notice attributes it to an uninspectable agent.
|
|
268
|
+
logger.warning(
|
|
269
|
+
"Agent 'tools' node is not introspectable (bound=%r); "
|
|
270
|
+
"LangGraph internals may have changed",
|
|
271
|
+
type(tool_node),
|
|
272
|
+
)
|
|
273
|
+
return None
|
|
274
|
+
tools: list[ToolEntry] = []
|
|
275
|
+
for name, tool in tools_by_name.items():
|
|
276
|
+
if not isinstance(name, str):
|
|
277
|
+
continue
|
|
278
|
+
description = getattr(tool, "description", None)
|
|
279
|
+
tools.append(
|
|
280
|
+
ToolEntry(
|
|
281
|
+
name=name,
|
|
282
|
+
description=_first_line(
|
|
283
|
+
description if isinstance(description, str) else None
|
|
284
|
+
),
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
return tools
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def collect_mcp_catalog(
|
|
291
|
+
*,
|
|
292
|
+
mcp_config_path: str | None = None,
|
|
293
|
+
trust_project_mcp: bool | None = None,
|
|
294
|
+
) -> tuple[list[ToolGroup], list[UnavailableServer], str | None]:
|
|
295
|
+
"""Discover MCP servers, split into tool groups and unavailable servers.
|
|
296
|
+
|
|
297
|
+
Best-effort: if discovery itself raises (no config, offline, load error),
|
|
298
|
+
the technical detail is logged and a generic `mcp_error` message is
|
|
299
|
+
returned so `dcode tools list` still renders the built-in tools while
|
|
300
|
+
telling the user discovery failed. Servers that loaded but expose no tools
|
|
301
|
+
are reported as `UnavailableServer`s (errored, needing login, or disabled)
|
|
302
|
+
rather than silently dropped — surfacing exactly what a user running this
|
|
303
|
+
command to debug a missing tool needs to see.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
mcp_config_path: Explicit MCP config path (`--mcp-config`), or `None`
|
|
307
|
+
to rely on auto-discovery.
|
|
308
|
+
trust_project_mcp: Project-level stdio trust decision
|
|
309
|
+
(`--trust-project-mcp`), forwarded to discovery unchanged.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
`(groups, unavailable, mcp_error)`: per-server tool groups, discovered
|
|
313
|
+
servers exposing no tools, and a generic discovery-failure message
|
|
314
|
+
(`None` when discovery succeeded).
|
|
315
|
+
"""
|
|
316
|
+
try:
|
|
317
|
+
server_info = asyncio.run(
|
|
318
|
+
_load_mcp_server_info(
|
|
319
|
+
mcp_config_path=mcp_config_path,
|
|
320
|
+
trust_project_mcp=trust_project_mcp,
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
except Exception:
|
|
324
|
+
# Log the real cause for debugging, but return a generic message so no
|
|
325
|
+
# file path or stack trace leaks into CLI/JSON output.
|
|
326
|
+
logger.warning("MCP tool discovery failed for `tools list`", exc_info=True)
|
|
327
|
+
return [], [], "MCP discovery failed; showing built-in tools only."
|
|
328
|
+
|
|
329
|
+
groups, unavailable = split_mcp_server_info(server_info)
|
|
330
|
+
return groups, unavailable, None
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def split_mcp_server_info(
|
|
334
|
+
server_info: Sequence[MCPServerInfo],
|
|
335
|
+
) -> tuple[list[ToolGroup], list[UnavailableServer]]:
|
|
336
|
+
"""Split loaded MCP server metadata into tool groups and unavailable servers.
|
|
337
|
+
|
|
338
|
+
Pure function shared by the CLI discovery path (`collect_mcp_catalog`) and
|
|
339
|
+
the interactive `/tools` command, which passes the app's already-loaded
|
|
340
|
+
`MCPServerInfo` list rather than re-discovering (Textual's running event
|
|
341
|
+
loop forbids the `asyncio.run` discovery path).
|
|
342
|
+
|
|
343
|
+
Servers that loaded but expose no tools are reported as `UnavailableServer`s
|
|
344
|
+
(errored, needing login, or disabled) rather than silently dropped —
|
|
345
|
+
surfacing exactly what a user debugging a missing tool needs to see.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
server_info: Loaded MCP server metadata.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
`(groups, unavailable)`: per-server tool groups (only servers exposing
|
|
352
|
+
tools) and servers discovered with no tools and a non-`ok` status.
|
|
353
|
+
"""
|
|
354
|
+
groups: list[ToolGroup] = []
|
|
355
|
+
unavailable: list[UnavailableServer] = []
|
|
356
|
+
for server in server_info:
|
|
357
|
+
if server.tools:
|
|
358
|
+
entries = tuple(
|
|
359
|
+
ToolEntry(name=tool.name, description=_first_line(tool.description))
|
|
360
|
+
for tool in server.tools
|
|
361
|
+
)
|
|
362
|
+
groups.append(ToolGroup(label=server.name, source="mcp", tools=entries))
|
|
363
|
+
elif server.status != "ok":
|
|
364
|
+
# A server that loaded but has no tools *and* is not "ok" is broken,
|
|
365
|
+
# unauthenticated, or disabled — report it so the omission is
|
|
366
|
+
# explained. A plainly-disabled server drops discovery's reason so
|
|
367
|
+
# the renderers show the generic "disabled by user" label; a
|
|
368
|
+
# just-re-enabled one (`pending_reconnect`) keeps its reconnect
|
|
369
|
+
# guidance so the renderer can distinguish it from a server the user
|
|
370
|
+
# left disabled. Other statuses retain discovery's reason string —
|
|
371
|
+
# not a stack trace, but config-load failures can include the local
|
|
372
|
+
# config file path — see `UnavailableServer.detail`.
|
|
373
|
+
detail = server.error or ""
|
|
374
|
+
if server.status == "disabled" and not server.pending_reconnect:
|
|
375
|
+
detail = ""
|
|
376
|
+
unavailable.append(
|
|
377
|
+
UnavailableServer(
|
|
378
|
+
name=server.name,
|
|
379
|
+
status=server.status,
|
|
380
|
+
detail=detail,
|
|
381
|
+
)
|
|
382
|
+
)
|
|
383
|
+
return groups, unavailable
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def build_catalog_from_server_info(
|
|
387
|
+
built_in: Sequence[ToolEntry],
|
|
388
|
+
server_info: Sequence[MCPServerInfo],
|
|
389
|
+
) -> ToolCatalog:
|
|
390
|
+
"""Assemble a `ToolCatalog` from pre-collected built-in tools and live MCP info.
|
|
391
|
+
|
|
392
|
+
The interactive `/tools` command entry point: it avoids the `asyncio.run`
|
|
393
|
+
MCP discovery reached via `collect_catalog` (the `asyncio.run` call itself
|
|
394
|
+
lives in `collect_mcp_catalog`), which cannot run inside Textual's running
|
|
395
|
+
event loop, by reusing the MCP metadata the app already loaded. `mcp_error`
|
|
396
|
+
is always `None` here because discovery is not attempted — any load failures
|
|
397
|
+
are already reflected per-server in `server_info` as non-`ok` `MCPServerInfo`
|
|
398
|
+
entries, which `split_mcp_server_info` surfaces as `UnavailableServer`s.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
built_in: Built-in tools in bind order (from `collect_built_in_tools`).
|
|
402
|
+
server_info: The app's already-loaded MCP server metadata.
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
A `ToolCatalog` with the built-in group first, then any MCP groups, plus
|
|
406
|
+
unavailable servers.
|
|
407
|
+
"""
|
|
408
|
+
groups: list[ToolGroup] = [
|
|
409
|
+
ToolGroup(label=BUILT_IN_GROUP, source="built-in", tools=tuple(built_in))
|
|
410
|
+
]
|
|
411
|
+
mcp_groups, unavailable = split_mcp_server_info(server_info)
|
|
412
|
+
groups.extend(mcp_groups)
|
|
413
|
+
return ToolCatalog(groups=tuple(groups), unavailable=tuple(unavailable))
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
async def _load_mcp_server_info(
|
|
417
|
+
*,
|
|
418
|
+
mcp_config_path: str | None,
|
|
419
|
+
trust_project_mcp: bool | None,
|
|
420
|
+
) -> list[Any]:
|
|
421
|
+
"""Load MCP server metadata, cleaning up any temporary sessions.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
mcp_config_path: Explicit MCP config path, or `None` for auto-discovery.
|
|
425
|
+
trust_project_mcp: Project-level stdio trust decision.
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
Discovered MCP server metadata, or an empty list when none load.
|
|
429
|
+
"""
|
|
430
|
+
from deepagents_code.mcp_tools import resolve_and_load_mcp_tools
|
|
431
|
+
from deepagents_code.project_utils import ProjectContext
|
|
432
|
+
|
|
433
|
+
try:
|
|
434
|
+
project_context = ProjectContext.from_user_cwd(Path.cwd())
|
|
435
|
+
except (OSError, RuntimeError):
|
|
436
|
+
# `Path.cwd()`/`.resolve()` raise OSError for a missing cwd and
|
|
437
|
+
# RuntimeError on a symlink loop (3.11-3.12); match the codebase's own
|
|
438
|
+
# convention in `project_utils` and fall back to no project context.
|
|
439
|
+
logger.warning("Could not determine working directory for MCP discovery")
|
|
440
|
+
project_context = None
|
|
441
|
+
|
|
442
|
+
session_manager = None
|
|
443
|
+
try:
|
|
444
|
+
_tools, session_manager, server_info = await resolve_and_load_mcp_tools(
|
|
445
|
+
explicit_config_path=mcp_config_path,
|
|
446
|
+
no_mcp=False,
|
|
447
|
+
trust_project_mcp=trust_project_mcp,
|
|
448
|
+
project_context=project_context,
|
|
449
|
+
)
|
|
450
|
+
return server_info or []
|
|
451
|
+
finally:
|
|
452
|
+
if session_manager is not None:
|
|
453
|
+
try:
|
|
454
|
+
await session_manager.cleanup()
|
|
455
|
+
except Exception:
|
|
456
|
+
logger.warning("MCP discovery cleanup failed", exc_info=True)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def collect_catalog(
|
|
460
|
+
*,
|
|
461
|
+
assistant_id: str = "agent",
|
|
462
|
+
enable_interpreter: bool = False,
|
|
463
|
+
include_mcp: bool = True,
|
|
464
|
+
mcp_config_path: str | None = None,
|
|
465
|
+
trust_project_mcp: bool | None = None,
|
|
466
|
+
) -> ToolCatalog:
|
|
467
|
+
"""Collect everything `dcode tools list` renders.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
assistant_id: Resolved dcode agent identifier to compile for built-in
|
|
471
|
+
tools, including any agent-specific subagents.
|
|
472
|
+
enable_interpreter: Whether the default agent binds `js_eval`; forwarded
|
|
473
|
+
to `collect_built_in_tools`.
|
|
474
|
+
include_mcp: When `True`, discover MCP servers and append their groups
|
|
475
|
+
after the built-in group (best-effort). Pass `False` to mirror
|
|
476
|
+
`--no-mcp`.
|
|
477
|
+
mcp_config_path: Explicit MCP config path (`--mcp-config`).
|
|
478
|
+
trust_project_mcp: Project-level stdio trust decision
|
|
479
|
+
(`--trust-project-mcp`).
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
A `ToolCatalog` with the built-in group first, then any MCP groups,
|
|
483
|
+
plus unavailable servers and any discovery-failure notice.
|
|
484
|
+
"""
|
|
485
|
+
groups: list[ToolGroup] = [
|
|
486
|
+
ToolGroup(
|
|
487
|
+
label=BUILT_IN_GROUP,
|
|
488
|
+
source="built-in",
|
|
489
|
+
tools=tuple(
|
|
490
|
+
collect_built_in_tools(
|
|
491
|
+
assistant_id=assistant_id,
|
|
492
|
+
enable_interpreter=enable_interpreter,
|
|
493
|
+
)
|
|
494
|
+
),
|
|
495
|
+
)
|
|
496
|
+
]
|
|
497
|
+
unavailable: list[UnavailableServer] = []
|
|
498
|
+
mcp_error: str | None = None
|
|
499
|
+
if include_mcp:
|
|
500
|
+
mcp_groups, unavailable, mcp_error = collect_mcp_catalog(
|
|
501
|
+
mcp_config_path=mcp_config_path,
|
|
502
|
+
trust_project_mcp=trust_project_mcp,
|
|
503
|
+
)
|
|
504
|
+
groups.extend(mcp_groups)
|
|
505
|
+
return ToolCatalog(
|
|
506
|
+
groups=tuple(groups),
|
|
507
|
+
unavailable=tuple(unavailable),
|
|
508
|
+
mcp_error=mcp_error,
|
|
509
|
+
)
|