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,540 @@
|
|
|
1
|
+
"""Server lifecycle orchestration for the app.
|
|
2
|
+
|
|
3
|
+
Provides `start_server_and_get_agent` which handles the full flow of:
|
|
4
|
+
|
|
5
|
+
1. Building a `ServerConfig` from application arguments
|
|
6
|
+
2. Writing config to env vars via `ServerConfig.to_env()`
|
|
7
|
+
3. Scaffolding a workspace (langgraph.json, checkpointer, pyproject)
|
|
8
|
+
4. Starting the `langgraph dev` server
|
|
9
|
+
5. Returning a `RemoteAgent` client
|
|
10
|
+
|
|
11
|
+
Also provides `server_session`, an async context manager that wraps
|
|
12
|
+
server startup and guaranteed cleanup so callers don't need to
|
|
13
|
+
duplicate try/finally teardown.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
import tempfile
|
|
21
|
+
from contextlib import asynccontextmanager
|
|
22
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import TYPE_CHECKING, Any
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from collections.abc import AsyncIterator
|
|
28
|
+
|
|
29
|
+
from deepagents_code.client.launch.server import ServerProcess
|
|
30
|
+
from deepagents_code.client.remote_client import RemoteAgent
|
|
31
|
+
from deepagents_code.mcp_tools import MCPSessionManager
|
|
32
|
+
|
|
33
|
+
from deepagents_code._env_vars import SERVER_ENV_PREFIX
|
|
34
|
+
from deepagents_code._server_config import ServerConfig
|
|
35
|
+
from deepagents_code.client.launch.server import _EPHEMERAL_PORT
|
|
36
|
+
from deepagents_code.project_utils import ProjectContext
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
_DISTRIBUTION_NAME = "deepagents-code"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _set_or_clear_server_env(name: str, value: str | None) -> None:
|
|
43
|
+
"""Set or clear a `DEEPAGENTS_CODE_SERVER_*` environment variable.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
name: Suffix after `DEEPAGENTS_CODE_SERVER_`.
|
|
47
|
+
value: String value to set, or `None` to clear the variable.
|
|
48
|
+
"""
|
|
49
|
+
key = f"{SERVER_ENV_PREFIX}{name}"
|
|
50
|
+
if value is None:
|
|
51
|
+
os.environ.pop(key, None)
|
|
52
|
+
else:
|
|
53
|
+
os.environ[key] = value
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _apply_server_config(config: ServerConfig) -> None:
|
|
57
|
+
"""Write a `ServerConfig` to `DEEPAGENTS_CODE_SERVER_*` env vars.
|
|
58
|
+
|
|
59
|
+
Uses `ServerConfig.to_env()` so that the set of variables and their
|
|
60
|
+
serialization format are defined in one place (the `ServerConfig` dataclass)
|
|
61
|
+
rather than maintained independently here and in the
|
|
62
|
+
reader (`ServerConfig.from_env()`).
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
config: Fully resolved server configuration.
|
|
66
|
+
"""
|
|
67
|
+
for suffix, value in config.to_env().items():
|
|
68
|
+
_set_or_clear_server_env(suffix, value)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _capture_project_context() -> ProjectContext | None:
|
|
72
|
+
"""Capture the user's project context for the server subprocess.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Explicit project context, or `None` when cwd cannot be determined.
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
return ProjectContext.from_user_cwd(Path.cwd())
|
|
79
|
+
except OSError:
|
|
80
|
+
logger.warning("Could not determine working directory for server")
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ------------------------------------------------------------------
|
|
85
|
+
# Workspace scaffolding
|
|
86
|
+
# ------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _scaffold_workspace(work_dir: Path) -> None:
|
|
90
|
+
"""Prepare the server working directory with all required files.
|
|
91
|
+
|
|
92
|
+
Generates the auxiliary files (checkpointer module, `pyproject.toml`,
|
|
93
|
+
`langgraph.json`) that `langgraph dev` needs to boot. The generated
|
|
94
|
+
graph reference imports the installed `deepagents_code` package directly.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
work_dir: Temporary directory that will become the server's cwd.
|
|
98
|
+
"""
|
|
99
|
+
from deepagents_code.client.launch.server import generate_langgraph_json
|
|
100
|
+
|
|
101
|
+
_write_checkpointer(work_dir)
|
|
102
|
+
_write_pyproject(work_dir)
|
|
103
|
+
|
|
104
|
+
# `graph_ref` is a dotted import of the installed `deepagents_code` package,
|
|
105
|
+
# but `checkpointer_path` stays cwd-relative: checkpointer.py is generated
|
|
106
|
+
# fresh into work_dir (which `ServerProcess.start()` sets as the subprocess
|
|
107
|
+
# cwd) and is not an importable package module. Don't "unify" these — a
|
|
108
|
+
# dotted ref for the checkpointer would fail to resolve.
|
|
109
|
+
generate_langgraph_json(
|
|
110
|
+
work_dir,
|
|
111
|
+
graph_ref="deepagents_code.server_graph:make_graph",
|
|
112
|
+
checkpointer_path="./checkpointer.py:create_checkpointer",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _write_checkpointer(work_dir: Path) -> None:
|
|
117
|
+
"""Write a checkpointer module that reads its DB path from the environment.
|
|
118
|
+
|
|
119
|
+
The generated module reads the DB path env var at runtime so the path
|
|
120
|
+
is never baked into generated source. This is consistent with the
|
|
121
|
+
`DEEPAGENTS_CODE_SERVER_*` env-var communication pattern used elsewhere.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
work_dir: Server working directory.
|
|
125
|
+
"""
|
|
126
|
+
from deepagents_code.sessions import get_db_path
|
|
127
|
+
|
|
128
|
+
# Set the env var that the generated module will read at import time.
|
|
129
|
+
os.environ[f"{SERVER_ENV_PREFIX}DB_PATH"] = str(get_db_path())
|
|
130
|
+
|
|
131
|
+
db_path_var = f"{SERVER_ENV_PREFIX}DB_PATH"
|
|
132
|
+
content = f'''\
|
|
133
|
+
"""Persistent SQLite checkpointer for the LangGraph dev server."""
|
|
134
|
+
|
|
135
|
+
import os
|
|
136
|
+
from contextlib import asynccontextmanager
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@asynccontextmanager
|
|
140
|
+
async def create_checkpointer():
|
|
141
|
+
"""Yield an AsyncSqliteSaver connected to the app's sessions DB.
|
|
142
|
+
|
|
143
|
+
The database path is read from the `{db_path_var}` env var
|
|
144
|
+
(set by the app before server startup) rather than hard-coded, so
|
|
145
|
+
the checkpointer module works without code generation.
|
|
146
|
+
"""
|
|
147
|
+
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
|
148
|
+
|
|
149
|
+
db_path = os.environ.get("{db_path_var}")
|
|
150
|
+
if not db_path:
|
|
151
|
+
raise RuntimeError(
|
|
152
|
+
"{db_path_var} not set. The app must set this "
|
|
153
|
+
"env var before server startup."
|
|
154
|
+
)
|
|
155
|
+
async with AsyncSqliteSaver.from_conn_string(db_path) as saver:
|
|
156
|
+
yield saver
|
|
157
|
+
'''
|
|
158
|
+
(work_dir / "checkpointer.py").write_text(content)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _write_pyproject(work_dir: Path) -> None:
|
|
162
|
+
"""Write a minimal pyproject.toml for the server working directory.
|
|
163
|
+
|
|
164
|
+
The `langgraph dev` server needs to install the project dependencies.
|
|
165
|
+
We point it at the app package which transitively pulls in the SDK.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
work_dir: Server working directory.
|
|
169
|
+
"""
|
|
170
|
+
content = f"""[project]
|
|
171
|
+
name = "deepagents-server-runtime"
|
|
172
|
+
version = "0.0.1"
|
|
173
|
+
requires-python = ">=3.11"
|
|
174
|
+
dependencies = [
|
|
175
|
+
"{_runtime_package_dependency()}",
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
[build-system]
|
|
179
|
+
requires = ["hatchling"]
|
|
180
|
+
build-backend = "hatchling.build"
|
|
181
|
+
"""
|
|
182
|
+
(work_dir / "pyproject.toml").write_text(content)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _default_package_project_root() -> Path | None:
|
|
186
|
+
"""Return the project root that contains the top-level package.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
The directory above `deepagents_code` — the editable project root in
|
|
190
|
+
source checkouts and `site-packages` for installed wheels — or
|
|
191
|
+
`None` when the package location cannot be determined (e.g. a frozen
|
|
192
|
+
or zipimport build where `__file__` is unset). Returning `None`
|
|
193
|
+
(rather than guessing `Path.cwd()`) lets the caller fall back to
|
|
194
|
+
the installed distribution version instead of pointing at whatever
|
|
195
|
+
unrelated project happens to sit in the launch directory.
|
|
196
|
+
"""
|
|
197
|
+
import deepagents_code
|
|
198
|
+
|
|
199
|
+
# `getattr` with a default: `__file__` is unset on frozen/zipimport builds
|
|
200
|
+
# and namespace packages, so it is not guaranteed to exist at runtime.
|
|
201
|
+
package_init = getattr(deepagents_code, "__file__", None)
|
|
202
|
+
if package_init is None:
|
|
203
|
+
return None
|
|
204
|
+
return Path(package_init).resolve().parent.parent
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _runtime_package_dependency(package_root: Path | None = None) -> str:
|
|
208
|
+
"""Return the dependency spec for the app package in the server runtime.
|
|
209
|
+
|
|
210
|
+
Editable source checkouts can use a local path dependency so the generated
|
|
211
|
+
runtime sees the working tree. Installed wheels cannot: the package parent is
|
|
212
|
+
`site-packages`, which is not an installable project. In that case, depend on
|
|
213
|
+
the installed distribution version instead.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
package_root: Optional package project root for tests.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Requirement string for the generated runtime `pyproject.toml`.
|
|
220
|
+
"""
|
|
221
|
+
root = package_root or _default_package_project_root()
|
|
222
|
+
if root is not None and (root / "pyproject.toml").is_file():
|
|
223
|
+
return f"{_DISTRIBUTION_NAME} @ {root.as_uri()}"
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
installed_version = version(_DISTRIBUTION_NAME)
|
|
227
|
+
except PackageNotFoundError:
|
|
228
|
+
return _DISTRIBUTION_NAME
|
|
229
|
+
return f"{_DISTRIBUTION_NAME}=={installed_version}"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# ------------------------------------------------------------------
|
|
233
|
+
# MCP pre-flight validation
|
|
234
|
+
# ------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _preflight_validate_mcp_config(
|
|
238
|
+
*,
|
|
239
|
+
mcp_config_path: str | None,
|
|
240
|
+
no_mcp: bool,
|
|
241
|
+
) -> None:
|
|
242
|
+
"""Validate the explicit `--mcp-config` path before spawning the server.
|
|
243
|
+
|
|
244
|
+
Catches the common failure mode of passing a malformed MCP config: a
|
|
245
|
+
`ValueError` raised inside the server subprocess otherwise surfaces as an
|
|
246
|
+
opaque truncated log dump in `wait_for_server_healthy`. Running the same
|
|
247
|
+
validation in the parent process lets the TUI display a clean, actionable
|
|
248
|
+
message with the offending path and reason.
|
|
249
|
+
|
|
250
|
+
Project-level and user-level configs discovered by `resolve_and_load_mcp_tools`
|
|
251
|
+
are not validated here; their errors are already handled leniently via
|
|
252
|
+
`load_mcp_config_with_error` and surface as errored entries in the
|
|
253
|
+
`/mcp` viewer rather than as a fatal startup failure.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
mcp_config_path: Explicit path passed via `--mcp-config`, or `None`.
|
|
257
|
+
no_mcp: When `True`, MCP is disabled and validation is skipped.
|
|
258
|
+
|
|
259
|
+
Raises:
|
|
260
|
+
MCPConfigError: If the config file is malformed or missing required
|
|
261
|
+
fields. Message includes the offending path for context.
|
|
262
|
+
"""
|
|
263
|
+
if no_mcp or not mcp_config_path:
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
from deepagents_code.mcp_tools import MCPConfigError, load_mcp_config
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
load_mcp_config(mcp_config_path)
|
|
270
|
+
except MCPConfigError:
|
|
271
|
+
raise
|
|
272
|
+
except FileNotFoundError as exc:
|
|
273
|
+
msg = f"MCP config file not found: {mcp_config_path}"
|
|
274
|
+
raise MCPConfigError(msg) from exc
|
|
275
|
+
except (ValueError, TypeError) as exc:
|
|
276
|
+
# `ValueError` covers `json.JSONDecodeError` (subclass) and the
|
|
277
|
+
# shape/field validators in `_validate_server_config`; `TypeError`
|
|
278
|
+
# covers the wrong-type branches. Bare `RuntimeError` is
|
|
279
|
+
# deliberately NOT caught — it would mask unrelated bugs
|
|
280
|
+
# (recursion, reentrancy, stdlib internals) as config errors.
|
|
281
|
+
msg = f"Invalid MCP config at {mcp_config_path}: {exc}"
|
|
282
|
+
raise MCPConfigError(msg) from exc
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# ------------------------------------------------------------------
|
|
286
|
+
# Server startup
|
|
287
|
+
# ------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
async def start_server_and_get_agent(
|
|
291
|
+
*,
|
|
292
|
+
assistant_id: str,
|
|
293
|
+
model_name: str | None = None,
|
|
294
|
+
model_params: dict[str, Any] | None = None,
|
|
295
|
+
auto_approve: bool = False,
|
|
296
|
+
interrupt_shell_only: bool = False,
|
|
297
|
+
shell_allow_list: list[str] | None = None,
|
|
298
|
+
sandbox_type: str = "none",
|
|
299
|
+
sandbox_id: str | None = None,
|
|
300
|
+
sandbox_snapshot_name: str | None = None,
|
|
301
|
+
sandbox_setup: str | None = None,
|
|
302
|
+
enable_shell: bool = True,
|
|
303
|
+
enable_ask_user: bool = False,
|
|
304
|
+
enable_interpreter: bool | None = None,
|
|
305
|
+
interpreter_ptc: str | list[str] | None = None,
|
|
306
|
+
interpreter_ptc_acknowledge_unsafe: bool = False,
|
|
307
|
+
rubric_model: str | None = None,
|
|
308
|
+
rubric_max_iterations: int | None = None,
|
|
309
|
+
mcp_config_path: str | None = None,
|
|
310
|
+
no_mcp: bool = False,
|
|
311
|
+
trust_project_mcp: bool | None = None,
|
|
312
|
+
interactive: bool = True,
|
|
313
|
+
host: str = "127.0.0.1",
|
|
314
|
+
port: int = _EPHEMERAL_PORT,
|
|
315
|
+
) -> tuple[RemoteAgent, ServerProcess, MCPSessionManager | None]:
|
|
316
|
+
"""Start a LangGraph server and return a connected remote agent client.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
assistant_id: Agent identifier.
|
|
320
|
+
model_name: Model spec string.
|
|
321
|
+
model_params: Extra model kwargs.
|
|
322
|
+
auto_approve: Auto-approve all tools.
|
|
323
|
+
interrupt_shell_only: Validate shell commands via middleware instead of HITL.
|
|
324
|
+
shell_allow_list: Restrictive shell allow-list for `ShellAllowListMiddleware`.
|
|
325
|
+
sandbox_type: Sandbox type.
|
|
326
|
+
sandbox_id: Existing sandbox ID to reuse.
|
|
327
|
+
sandbox_snapshot_name: Snapshot (langsmith) or blueprint (runloop) name.
|
|
328
|
+
sandbox_setup: Path to setup script for the sandbox.
|
|
329
|
+
enable_shell: Enable shell execution tools.
|
|
330
|
+
enable_ask_user: Enable ask_user tool.
|
|
331
|
+
enable_interpreter: Enable the JS interpreter (`js_eval`) middleware on
|
|
332
|
+
the main agent. `None` uses the sandbox-aware default.
|
|
333
|
+
interpreter_ptc: Override for `settings.interpreter_ptc` (PTC allowlist).
|
|
334
|
+
interpreter_ptc_acknowledge_unsafe: Explicit acknowledgement for
|
|
335
|
+
`interpreter_ptc="all"` outside of `auto_approve`.
|
|
336
|
+
rubric_model: Grader model spec; `None` reuses the main model.
|
|
337
|
+
rubric_max_iterations: Explicit grader iterations per rubric attempt;
|
|
338
|
+
`None` uses the SDK default.
|
|
339
|
+
mcp_config_path: Path to MCP config.
|
|
340
|
+
no_mcp: Disable MCP.
|
|
341
|
+
trust_project_mcp: Trust project MCP servers.
|
|
342
|
+
interactive: Whether the agent is interactive.
|
|
343
|
+
host: Server host.
|
|
344
|
+
port: Server port. Defaults to `_EPHEMERAL_PORT` (0), letting the server
|
|
345
|
+
pick a free ephemeral port instead of the well-known `langgraph dev`
|
|
346
|
+
port 2024.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
Tuple of `(remote_agent, server_process, mcp_session_manager)`.
|
|
350
|
+
The `mcp_session_manager` is currently always `None` (MCP lifecycle
|
|
351
|
+
is handled server-side).
|
|
352
|
+
|
|
353
|
+
Raises:
|
|
354
|
+
MCPConfigError: The explicit `--mcp-config` path is malformed,
|
|
355
|
+
missing, or references contradictory transport fields. Raised
|
|
356
|
+
from the pre-flight validator before any subprocess is spawned.
|
|
357
|
+
""" # noqa: DOC502 - `_preflight_validate_mcp_config()` raises indirectly
|
|
358
|
+
from deepagents_code.client.launch.server import ServerProcess
|
|
359
|
+
from deepagents_code.client.remote_client import RemoteAgent
|
|
360
|
+
|
|
361
|
+
project_context = _capture_project_context()
|
|
362
|
+
|
|
363
|
+
_preflight_validate_mcp_config(
|
|
364
|
+
mcp_config_path=mcp_config_path,
|
|
365
|
+
no_mcp=no_mcp,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
config = ServerConfig.from_cli_args(
|
|
369
|
+
project_context=project_context,
|
|
370
|
+
model_name=model_name,
|
|
371
|
+
model_params=model_params,
|
|
372
|
+
assistant_id=assistant_id,
|
|
373
|
+
auto_approve=auto_approve,
|
|
374
|
+
interrupt_shell_only=interrupt_shell_only,
|
|
375
|
+
shell_allow_list=shell_allow_list,
|
|
376
|
+
sandbox_type=sandbox_type,
|
|
377
|
+
sandbox_id=sandbox_id,
|
|
378
|
+
sandbox_snapshot_name=sandbox_snapshot_name,
|
|
379
|
+
sandbox_setup=sandbox_setup,
|
|
380
|
+
enable_shell=enable_shell,
|
|
381
|
+
enable_ask_user=enable_ask_user,
|
|
382
|
+
enable_interpreter=enable_interpreter,
|
|
383
|
+
interpreter_ptc=interpreter_ptc,
|
|
384
|
+
interpreter_ptc_acknowledge_unsafe=interpreter_ptc_acknowledge_unsafe,
|
|
385
|
+
rubric_model=rubric_model,
|
|
386
|
+
rubric_max_iterations=rubric_max_iterations,
|
|
387
|
+
mcp_config_path=mcp_config_path,
|
|
388
|
+
no_mcp=no_mcp,
|
|
389
|
+
trust_project_mcp=trust_project_mcp,
|
|
390
|
+
interactive=interactive,
|
|
391
|
+
)
|
|
392
|
+
_apply_server_config(config)
|
|
393
|
+
|
|
394
|
+
work_dir = Path(tempfile.mkdtemp(prefix="deepagents_server_"))
|
|
395
|
+
_scaffold_workspace(work_dir)
|
|
396
|
+
|
|
397
|
+
server = ServerProcess(
|
|
398
|
+
host=host,
|
|
399
|
+
port=port,
|
|
400
|
+
config_dir=work_dir,
|
|
401
|
+
owns_config_dir=True,
|
|
402
|
+
scaffold=_scaffold_workspace,
|
|
403
|
+
)
|
|
404
|
+
started = False
|
|
405
|
+
try:
|
|
406
|
+
await server.start()
|
|
407
|
+
await server.wait_for_graph_ready("agent")
|
|
408
|
+
agent = RemoteAgent(
|
|
409
|
+
url=server.url,
|
|
410
|
+
graph_name="agent",
|
|
411
|
+
)
|
|
412
|
+
started = True
|
|
413
|
+
return agent, server, None
|
|
414
|
+
finally:
|
|
415
|
+
if not started:
|
|
416
|
+
# Startup failed or was cancelled before the server was handed off
|
|
417
|
+
# to the caller (which records the reference only on success). If
|
|
418
|
+
# `start()` itself failed it already reaped its own subprocess, so
|
|
419
|
+
# this `stop()` is then an idempotent no-op; this cleanup is the sole
|
|
420
|
+
# reaper only when `start()` succeeded but `wait_for_graph_ready()`
|
|
421
|
+
# (or `RemoteAgent()`) failed afterward. A `finally` rather than
|
|
422
|
+
# `except Exception` is deliberate: `asyncio.CancelledError` is a
|
|
423
|
+
# `BaseException`, so an `except Exception` guard would skip cleanup
|
|
424
|
+
# and orphan the process. The inner guard stops a `stop()` error
|
|
425
|
+
# from masking the exception already propagating.
|
|
426
|
+
try:
|
|
427
|
+
server.stop()
|
|
428
|
+
except Exception:
|
|
429
|
+
logger.exception(
|
|
430
|
+
"Error stopping server during startup cleanup",
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
# ------------------------------------------------------------------
|
|
435
|
+
# Session context manager
|
|
436
|
+
# ------------------------------------------------------------------
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
@asynccontextmanager
|
|
440
|
+
async def server_session(
|
|
441
|
+
*,
|
|
442
|
+
assistant_id: str,
|
|
443
|
+
model_name: str | None = None,
|
|
444
|
+
model_params: dict[str, Any] | None = None,
|
|
445
|
+
auto_approve: bool = False,
|
|
446
|
+
interrupt_shell_only: bool = False,
|
|
447
|
+
shell_allow_list: list[str] | None = None,
|
|
448
|
+
sandbox_type: str = "none",
|
|
449
|
+
sandbox_id: str | None = None,
|
|
450
|
+
sandbox_snapshot_name: str | None = None,
|
|
451
|
+
sandbox_setup: str | None = None,
|
|
452
|
+
enable_shell: bool = True,
|
|
453
|
+
enable_ask_user: bool = False,
|
|
454
|
+
enable_interpreter: bool | None = None,
|
|
455
|
+
interpreter_ptc: str | list[str] | None = None,
|
|
456
|
+
interpreter_ptc_acknowledge_unsafe: bool = False,
|
|
457
|
+
rubric_model: str | None = None,
|
|
458
|
+
rubric_max_iterations: int | None = None,
|
|
459
|
+
mcp_config_path: str | None = None,
|
|
460
|
+
no_mcp: bool = False,
|
|
461
|
+
trust_project_mcp: bool | None = None,
|
|
462
|
+
interactive: bool = True,
|
|
463
|
+
host: str = "127.0.0.1",
|
|
464
|
+
port: int = _EPHEMERAL_PORT,
|
|
465
|
+
) -> AsyncIterator[tuple[RemoteAgent, ServerProcess]]:
|
|
466
|
+
"""Async context manager that starts a server and guarantees cleanup.
|
|
467
|
+
|
|
468
|
+
Wraps `start_server_and_get_agent` so callers don't need to duplicate the
|
|
469
|
+
try/finally pattern for stopping the server.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
assistant_id: Agent identifier.
|
|
473
|
+
model_name: Model spec string.
|
|
474
|
+
model_params: Extra model kwargs.
|
|
475
|
+
auto_approve: Auto-approve all tools.
|
|
476
|
+
interrupt_shell_only: Validate shell commands via middleware instead of HITL.
|
|
477
|
+
shell_allow_list: Restrictive shell allow-list for `ShellAllowListMiddleware`.
|
|
478
|
+
sandbox_type: Sandbox type.
|
|
479
|
+
sandbox_id: Existing sandbox ID to reuse.
|
|
480
|
+
sandbox_snapshot_name: Snapshot (langsmith) or blueprint (runloop) name.
|
|
481
|
+
sandbox_setup: Path to setup script for the sandbox.
|
|
482
|
+
enable_shell: Enable shell execution tools.
|
|
483
|
+
enable_ask_user: Enable ask_user tool.
|
|
484
|
+
enable_interpreter: Enable the JS interpreter (`js_eval`) middleware on
|
|
485
|
+
the main agent. `None` uses the sandbox-aware default.
|
|
486
|
+
interpreter_ptc: Override for `settings.interpreter_ptc` (PTC allowlist).
|
|
487
|
+
interpreter_ptc_acknowledge_unsafe: Explicit acknowledgement for
|
|
488
|
+
`interpreter_ptc="all"` outside of `auto_approve`.
|
|
489
|
+
rubric_model: Grader model spec; `None` reuses the main model.
|
|
490
|
+
rubric_max_iterations: Explicit grader iterations per rubric attempt;
|
|
491
|
+
`None` uses the SDK default.
|
|
492
|
+
mcp_config_path: Path to MCP config.
|
|
493
|
+
no_mcp: Disable MCP.
|
|
494
|
+
trust_project_mcp: Trust project MCP servers.
|
|
495
|
+
interactive: Whether the agent is interactive.
|
|
496
|
+
host: Server host.
|
|
497
|
+
port: Server port. Defaults to `_EPHEMERAL_PORT` (0), letting the server
|
|
498
|
+
pick a free ephemeral port instead of the well-known `langgraph dev`
|
|
499
|
+
port 2024.
|
|
500
|
+
|
|
501
|
+
Yields:
|
|
502
|
+
Tuple of `(remote_agent, server_process)`.
|
|
503
|
+
"""
|
|
504
|
+
server_proc: ServerProcess | None = None
|
|
505
|
+
mcp_session_manager: MCPSessionManager | None = None
|
|
506
|
+
try:
|
|
507
|
+
agent, server_proc, mcp_session_manager = await start_server_and_get_agent(
|
|
508
|
+
assistant_id=assistant_id,
|
|
509
|
+
model_name=model_name,
|
|
510
|
+
model_params=model_params,
|
|
511
|
+
auto_approve=auto_approve,
|
|
512
|
+
interrupt_shell_only=interrupt_shell_only,
|
|
513
|
+
shell_allow_list=shell_allow_list,
|
|
514
|
+
sandbox_type=sandbox_type,
|
|
515
|
+
sandbox_id=sandbox_id,
|
|
516
|
+
sandbox_snapshot_name=sandbox_snapshot_name,
|
|
517
|
+
sandbox_setup=sandbox_setup,
|
|
518
|
+
enable_shell=enable_shell,
|
|
519
|
+
enable_ask_user=enable_ask_user,
|
|
520
|
+
enable_interpreter=enable_interpreter,
|
|
521
|
+
interpreter_ptc=interpreter_ptc,
|
|
522
|
+
interpreter_ptc_acknowledge_unsafe=interpreter_ptc_acknowledge_unsafe,
|
|
523
|
+
rubric_model=rubric_model,
|
|
524
|
+
rubric_max_iterations=rubric_max_iterations,
|
|
525
|
+
mcp_config_path=mcp_config_path,
|
|
526
|
+
no_mcp=no_mcp,
|
|
527
|
+
trust_project_mcp=trust_project_mcp,
|
|
528
|
+
interactive=interactive,
|
|
529
|
+
host=host,
|
|
530
|
+
port=port,
|
|
531
|
+
)
|
|
532
|
+
yield agent, server_proc
|
|
533
|
+
finally:
|
|
534
|
+
if mcp_session_manager is not None:
|
|
535
|
+
try:
|
|
536
|
+
await mcp_session_manager.cleanup()
|
|
537
|
+
except Exception:
|
|
538
|
+
logger.warning("MCP session cleanup failed", exc_info=True)
|
|
539
|
+
if server_proc is not None:
|
|
540
|
+
server_proc.stop()
|