dex-agents-deepagents-cli 0.6.0__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.
@@ -0,0 +1,382 @@
1
+ # Copyright (c) 2026 Super Durable, Inc.
2
+ #
3
+ # Licensed under the Sustainable Use License 1.0.
4
+ # You may not use this file except in compliance with the License.
5
+ # See the LICENSE file in the repository root.
6
+ #
7
+ # SPDX-License-Identifier: LicenseRef-Sustainable-Use-1.0
8
+
9
+ """The Deep Agents backend of the command: real files in the workspace and its shell."""
10
+
11
+ from __future__ import annotations
12
+
13
+ import contextlib
14
+ import dataclasses
15
+ import hashlib
16
+ import logging
17
+ import os
18
+ import shutil
19
+ import stat
20
+ import time
21
+ from collections.abc import Iterator
22
+ from contextvars import ContextVar
23
+ from datetime import timedelta
24
+ from pathlib import Path
25
+
26
+ from deepagents.backends import CompositeBackend, FilesystemBackend, LocalShellBackend
27
+ from deepagents.backends.protocol import (
28
+ DEFAULT_GREP_TIMEOUT,
29
+ BackendProtocol,
30
+ GlobResult,
31
+ GrepResult,
32
+ LsResult,
33
+ )
34
+ from deepagents.backends.utils import compile_grep_include_glob
35
+
36
+ #: The prefixes Deep Agents' ``FilesystemMiddleware`` writes its own files under.
37
+ DEEP_AGENTS_ARTIFACT_PREFIXES = ("/large_tool_results/", "/conversation_history/")
38
+
39
+ #: How long a session's artifacts stay after their last change.
40
+ SESSION_ARTIFACTS_RETENTION = timedelta(days=30)
41
+
42
+ _LOGGER = logging.getLogger(__name__)
43
+
44
+ _RUNNING_SESSION_ID: ContextVar[str | None] = ContextVar(
45
+ "dex_agents_deepagents_running_session_id", default=None
46
+ )
47
+
48
+ # Search results keyed by path, each with its matching (line number, text) pairs.
49
+ _SearchResults = dict[str, list[tuple[int, str]]]
50
+
51
+
52
+ class WorkspaceShellBackend(LocalShellBackend):
53
+ """A ``LocalShellBackend`` on the host's paths whose file tools stay in the workspace.
54
+
55
+ File tools take absolute host paths, as Deep Agents' local mode does, so
56
+ a file tool and ``execute`` name a file the same way. A path that
57
+ resolves outside the workspace, through ``..`` or a symbolic link, fails
58
+ the file operation with an error the model sees. ``ls``, ``glob``, and
59
+ ``grep`` leave out every entry whose real path is outside the workspace,
60
+ and ``grep`` never opens such a file.
61
+
62
+ This is a guardrail against mistaken paths, not a sandbox. ``execute``
63
+ runs ``/bin/sh`` in the workspace with the user's permissions and can
64
+ reach any path. A link swapped during a call and a hard link to an
65
+ outside file are not detected.
66
+ """
67
+
68
+ def __init__(self, workspace: Path) -> None:
69
+ """Root the backend at the workspace.
70
+
71
+ Args:
72
+ workspace: The absolute workspace directory.
73
+
74
+ Raises:
75
+ RuntimeError: If the installed deepagents lacks the method the
76
+ confinement overrides.
77
+ """
78
+ if not callable(getattr(LocalShellBackend, "_resolve_path", None)):
79
+ # The confinement overrides this private method; the dependency range pins it.
80
+ raise RuntimeError(
81
+ "this deepagents release has no FilesystemBackend._resolve_path; install the "
82
+ "version range dex-agents-deepagents-cli declares"
83
+ )
84
+ super().__init__(root_dir=workspace, virtual_mode=False, inherit_env=True)
85
+ self._workspace_real_path = Path(os.path.realpath(workspace))
86
+
87
+ def ls(self, path: str) -> LsResult:
88
+ """List a directory, without the entries whose real path leaves the workspace."""
89
+ result = super().ls(path)
90
+ if result.entries is None:
91
+ return result
92
+ entries = [entry for entry in result.entries if self._is_inside(entry["path"])]
93
+ return dataclasses.replace(result, entries=entries)
94
+
95
+ def glob(self, pattern: str, path: str | None = None) -> GlobResult:
96
+ """Find files, without the matches whose real path leaves the workspace."""
97
+ result = super().glob(pattern, path)
98
+ if result.matches is None:
99
+ return result
100
+ matches = [match for match in result.matches if self._is_inside(match["path"])]
101
+ return dataclasses.replace(result, matches=matches)
102
+
103
+ def grep(
104
+ self,
105
+ pattern: str,
106
+ path: str | None = None,
107
+ glob: str | None = None,
108
+ *,
109
+ max_count: int | None = None,
110
+ context_lines: int = 0,
111
+ ) -> GrepResult:
112
+ """Search files, without the matches in files whose real path leaves the workspace."""
113
+ result = super().grep(pattern, path, glob, max_count=max_count, context_lines=context_lines)
114
+ if result.matches is None:
115
+ return result
116
+ matches = [match for match in result.matches if self._is_inside(match["path"])]
117
+ return dataclasses.replace(result, matches=matches)
118
+
119
+ def _python_search(
120
+ self,
121
+ pattern: str,
122
+ base_full: Path,
123
+ include_glob: str | None,
124
+ *,
125
+ max_count: int | None = None,
126
+ timeout: int = DEFAULT_GREP_TIMEOUT,
127
+ ) -> tuple[_SearchResults, bool, str | None]:
128
+ # Deep Agents' fallback opens every file it walks; this one checks each real path first.
129
+ deadline = time.monotonic() + timeout
130
+ matches_glob = compile_grep_include_glob(include_glob) if include_glob else None
131
+ root = base_full if base_full.is_dir() else base_full.parent
132
+ candidates = (
133
+ [base_full]
134
+ if base_full.is_file()
135
+ else (
136
+ Path(directory) / name
137
+ for directory, _, names in os.walk(root, followlinks=False)
138
+ for name in names
139
+ )
140
+ )
141
+ results: _SearchResults = {}
142
+ unreadable: list[str] = []
143
+ total = 0
144
+ for candidate in candidates:
145
+ if time.monotonic() > deadline:
146
+ return results, True, None
147
+ if matches_glob is not None and not matches_glob(
148
+ candidate.relative_to(root).as_posix()
149
+ ):
150
+ continue
151
+ if not self._is_inside(str(candidate)) or not _is_small_file(
152
+ candidate, self.max_file_size_bytes
153
+ ):
154
+ continue
155
+ try:
156
+ with candidate.open(encoding="utf-8", errors="strict") as handle:
157
+ for line_number, line in enumerate(handle, 1):
158
+ if pattern not in line:
159
+ continue
160
+ if max_count is not None and total >= max_count:
161
+ return results, True, None
162
+ results.setdefault(str(candidate), []).append(
163
+ (line_number, line.rstrip("\n"))
164
+ )
165
+ total += 1
166
+ except UnicodeDecodeError:
167
+ # Binary files are skipped, as ripgrep does.
168
+ continue
169
+ except OSError as error:
170
+ unreadable.append(f"- {candidate}: {type(error).__name__}: {error.strerror}")
171
+ if not unreadable:
172
+ return results, False, None
173
+ return (
174
+ results,
175
+ False,
176
+ "One or more files could not be fully searched:\n" + "\n".join(unreadable),
177
+ )
178
+
179
+ def _resolve_path(self, key: str) -> Path:
180
+ resolved = super()._resolve_path(key)
181
+ if not self._is_inside(str(resolved)):
182
+ # An OSError, so each file operation reports it as its own error result.
183
+ raise PermissionError(
184
+ f"{key} is outside the workspace {self._workspace_real_path}; file tools "
185
+ "may only use paths inside it"
186
+ )
187
+ return resolved
188
+
189
+ def _is_inside(self, path: str) -> bool:
190
+ # realpath also follows a dangling link to the target a write would create.
191
+ real_path = Path(os.path.realpath(self.cwd / path))
192
+ return real_path.is_relative_to(self._workspace_real_path)
193
+
194
+
195
+ class SessionArtifactsBackend(FilesystemBackend):
196
+ """A virtual ``FilesystemBackend`` whose root is one private directory per session.
197
+
198
+ Deep Agents names a large tool result after its tool call ID, which two
199
+ sessions may share. Each session therefore gets its own directory,
200
+ ``<artifacts>/<SHA-256 of the session ID>/<name>``. The name is stable,
201
+ so a Worker that restarts on the same machine finds the files. Every
202
+ file operation first checks each directory of that path: the user owns
203
+ it, it is no symbolic link, and its mode becomes 0700 when it is looser.
204
+ A failed check, or an operation outside a running model call or tool
205
+ Step, returns the operation's error result.
206
+ """
207
+
208
+ def __init__(self, artifacts_directory: Path, name: str) -> None:
209
+ """Keep the per-user directory and the name of this kind of file.
210
+
211
+ Args:
212
+ artifacts_directory: The private per-user directory, such as
213
+ ``default_artifacts_directory()``.
214
+ name: The subdirectory of each session, such as ``large_tool_results``.
215
+ """
216
+ self._artifacts_directory = artifacts_directory
217
+ self._name = name
218
+ super().__init__(root_dir=artifacts_directory, virtual_mode=True)
219
+
220
+ @property
221
+ def cwd(self) -> Path:
222
+ """Return the running session's checked directory.
223
+
224
+ Raises:
225
+ PermissionError: Outside a running session, or when a directory of
226
+ the path fails its check. Every file operation reports it as an error.
227
+ """
228
+ session_id = _RUNNING_SESSION_ID.get()
229
+ if session_id is None:
230
+ raise PermissionError("Deep Agents' own files are available only while a session runs")
231
+ artifacts = ensure_artifacts_directory(self._artifacts_directory)
232
+ session_directory = _private_directory(
233
+ artifacts / hashlib.sha256(session_id.encode()).hexdigest()
234
+ )
235
+ # The virtual root check compares resolved paths.
236
+ return _private_directory(session_directory / self._name).resolve()
237
+
238
+ @cwd.setter
239
+ def cwd(self, root_dir: Path) -> None:
240
+ # FilesystemBackend's constructor assigns its root; each session's root replaces it.
241
+ return None
242
+
243
+
244
+ @contextlib.contextmanager
245
+ def bind_artifacts_session(session_id: str) -> Iterator[None]:
246
+ """Make Deep Agents' own files those of one session while the block runs.
247
+
248
+ Args:
249
+ session_id: The session ID, which is the Flow ID.
250
+ """
251
+ token = _RUNNING_SESSION_ID.set(session_id)
252
+ try:
253
+ yield
254
+ finally:
255
+ _RUNNING_SESSION_ID.reset(token)
256
+
257
+
258
+ def build_workspace_backend(
259
+ workspace: Path, artifacts_directory: Path | None = None
260
+ ) -> CompositeBackend:
261
+ """Build the command's backend: the workspace, with Deep Agents' own files kept apart.
262
+
263
+ Deep Agents writes large tool results and long user messages under
264
+ ``/large_tool_results/`` and ``/conversation_history/``. Those prefixes go
265
+ to the running session's private directory, so they never land at the
266
+ file system root, in the workspace, or in another session.
267
+
268
+ Args:
269
+ workspace: The absolute workspace directory.
270
+ artifacts_directory: The per-user directory of Deep Agents' own
271
+ files; ``None`` uses ``default_artifacts_directory()``.
272
+
273
+ Returns:
274
+ The backend for ``create_deep_agent``.
275
+ """
276
+ artifacts = artifacts_directory or default_artifacts_directory()
277
+ routes: dict[str, BackendProtocol] = {
278
+ prefix: SessionArtifactsBackend(artifacts, prefix.strip("/"))
279
+ for prefix in DEEP_AGENTS_ARTIFACT_PREFIXES
280
+ }
281
+ return CompositeBackend(default=WorkspaceShellBackend(workspace), routes=routes)
282
+
283
+
284
+ def default_artifacts_directory() -> Path:
285
+ """Return the per-user directory of Deep Agents' own files, without creating it.
286
+
287
+ Returns:
288
+ ``~/.dex/agents/deepagents-artifacts``.
289
+ """
290
+ return Path.home() / ".dex" / "agents" / "deepagents-artifacts"
291
+
292
+
293
+ def ensure_artifacts_directory(artifacts_directory: Path) -> Path:
294
+ """Create the per-user artifacts directory and check it and its two parents.
295
+
296
+ The parents, such as ``~/.dex`` and ``~/.dex/agents``, keep their mode,
297
+ but neither may be a symbolic link that another user owns.
298
+
299
+ Args:
300
+ artifacts_directory: The per-user directory.
301
+
302
+ Returns:
303
+ The directory, owned by the user, mode 0700.
304
+
305
+ Raises:
306
+ PermissionError: If a parent is another user's link, or the directory
307
+ is a link or another user's.
308
+ """
309
+ for parent in (artifacts_directory.parent.parent, artifacts_directory.parent):
310
+ if parent.is_symlink() and parent.lstat().st_uid != os.getuid():
311
+ raise PermissionError(f"{parent} is a link that another user owns")
312
+ artifacts_directory.parent.mkdir(parents=True, exist_ok=True)
313
+ return _private_directory(artifacts_directory)
314
+
315
+
316
+ def remove_stale_session_artifacts(
317
+ artifacts_directory: Path, retention: timedelta = SESSION_ARTIFACTS_RETENTION
318
+ ) -> list[Path]:
319
+ """Delete the session directories whose files nothing changed within the retention.
320
+
321
+ The command calls it once when it starts. It never follows a link, and
322
+ a directory it cannot read or delete stays and is logged.
323
+
324
+ Args:
325
+ artifacts_directory: The per-user directory.
326
+ retention: How long after its last change a session's files stay.
327
+
328
+ Returns:
329
+ The deleted session directories.
330
+ """
331
+ if not artifacts_directory.is_dir() or artifacts_directory.is_symlink():
332
+ return []
333
+ cutoff = time.time() - retention.total_seconds()
334
+ removed: list[Path] = []
335
+ try:
336
+ session_directories = list(artifacts_directory.iterdir())
337
+ except OSError as error:
338
+ _LOGGER.warning("cannot list session artifacts in %s: %s", artifacts_directory, error)
339
+ return removed
340
+ for session_directory in session_directories:
341
+ try:
342
+ if not stat.S_ISDIR(session_directory.lstat().st_mode):
343
+ continue
344
+ if _latest_change(session_directory) >= cutoff:
345
+ continue
346
+ shutil.rmtree(session_directory)
347
+ except OSError as error:
348
+ _LOGGER.warning(
349
+ "cannot remove stale session artifacts %s: %s", session_directory, error
350
+ )
351
+ continue
352
+ _LOGGER.info("removed stale session artifacts %s", session_directory)
353
+ removed.append(session_directory)
354
+ return removed
355
+
356
+
357
+ def _latest_change(directory: Path) -> float:
358
+ latest = directory.lstat().st_mtime
359
+ for parent, directory_names, file_names in os.walk(directory, followlinks=False):
360
+ for name in (*directory_names, *file_names):
361
+ latest = max(latest, (Path(parent) / name).lstat().st_mtime)
362
+ return latest
363
+
364
+
365
+ def _private_directory(directory: Path) -> Path:
366
+ directory.mkdir(mode=0o700, exist_ok=True)
367
+ status = directory.lstat()
368
+ if stat.S_ISLNK(status.st_mode) or not stat.S_ISDIR(status.st_mode):
369
+ raise PermissionError(f"{directory} is not a directory")
370
+ if status.st_uid != os.getuid():
371
+ raise PermissionError(f"{directory} belongs to another user")
372
+ if stat.S_IMODE(status.st_mode) & 0o077:
373
+ directory.chmod(0o700)
374
+ return directory
375
+
376
+
377
+ def _is_small_file(path: Path, max_bytes: int) -> bool:
378
+ try:
379
+ status = path.stat()
380
+ except OSError:
381
+ return False
382
+ return stat.S_ISREG(status.st_mode) and status.st_size <= max_bytes
@@ -0,0 +1,199 @@
1
+ Metadata-Version: 2.4
2
+ Name: dex-agents-deepagents-cli
3
+ Version: 0.6.0
4
+ Summary: The dex-agents-deepagents command: a durable Deep Agents agent in your terminal
5
+ Author: Super Durable
6
+ License-Expression: LicenseRef-Sustainable-Use-1.0
7
+ License-File: LICENSE
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Classifier: Typing :: Typed
12
+ Requires-Dist: deepagents>=0.7.32,<0.8
13
+ Requires-Dist: dex-agents==0.6.0
14
+ Requires-Dist: dex-agents-cli-core==0.6.0
15
+ Requires-Dist: dex-agents-langgraph==0.6.0
16
+ Requires-Dist: dex-python-sdk==1.45.0
17
+ Requires-Dist: langchain>=1.4.7,<2
18
+ Requires-Dist: langchain-core>=1.7.3,<1.8
19
+ Requires-Dist: langchain-openai>=1.0,<2
20
+ Requires-Dist: langgraph>=1.2.19,<1.3
21
+ Requires-Python: >=3.11
22
+ Project-URL: Homepage, https://github.com/superdurable/dex/tree/main/agents/cli/python/deepagents
23
+ Project-URL: Repository, https://github.com/superdurable/dex
24
+ Project-URL: Changelog, https://github.com/superdurable/dex/blob/main/agents/CHANGELOG.md
25
+ Description-Content-Type: text/markdown
26
+
27
+ # dex-agents-deepagents-cli
28
+
29
+ `dex-agents-deepagents-cli` provides the `dex-agents-deepagents` command. It
30
+ runs a [Deep Agents](https://github.com/langchain-ai/deepagents) agent as a
31
+ durable Dex agent session and chats with it in your terminal. The shared
32
+ CLI core, [`dex-agents-cli-core`](../core/README.md), starts or reuses
33
+ `dexcli dev`, connects the connectors MCP server, and runs the Worker. This
34
+ package only builds the agent with `create_deep_agent` and runs it on the
35
+ [LangGraph adapter](../../../python/langgraph/README.md).
36
+
37
+ ## Running it
38
+
39
+ The command needs Python 3.11 or later and a key for the model's provider.
40
+ The default model is OpenAI's, so set `OPENAI_API_KEY`:
41
+
42
+ ```bash
43
+ export OPENAI_API_KEY=sk-...
44
+ uvx --from dex-agents-deepagents-cli dex-agents-deepagents
45
+ ```
46
+
47
+ The first run asks the `init` questions and writes
48
+ `~/.dex/agents/deepagents.yaml`. Then type a message at the `>` prompt.
49
+ Approve or reject tool calls with `y` or `n`, and press Ctrl-D to exit. The
50
+ session stays in Dex, and the command prints the `--resume` line that
51
+ continues it. See the [CLI core README](../core/README.md#running-a-command)
52
+ for every subcommand and option, and [`agents/cli`](../../README.md) for the
53
+ configuration file.
54
+
55
+ The command never prints or stores provider keys. The chat model reads its
56
+ key from the provider's standard environment variable.
57
+
58
+ ## Configuration
59
+
60
+ `model` is a LangChain `provider:model` ID, as `init_chat_model` takes it. It
61
+ defaults to `openai:gpt-5.2`; set `model` in the file or pass `--model` to
62
+ change it. An `openai:` model uses the Responses API, as Deep Agents does.
63
+
64
+ | Provider | Model example | Environment |
65
+ | --- | --- | --- |
66
+ | OpenAI | `openai:gpt-5.2` | `OPENAI_API_KEY`, optional `OPENAI_BASE_URL` |
67
+ | Anthropic | `anthropic:claude-opus-5-5` | `ANTHROPIC_API_KEY` |
68
+ | Google | `google_genai:gemini-2.5-flash` | `GOOGLE_API_KEY` |
69
+
70
+ Other providers need their LangChain integration package, such as
71
+ `uvx --from dex-agents-deepagents-cli --with langchain-mistralai dex-agents-deepagents`.
72
+
73
+ `system_prompt` replaces the default instructions. Session settings may
74
+ change the model and the instructions from the next turn on. The
75
+ `deepagents` section has no options yet, and any key in it is a configuration
76
+ error.
77
+
78
+ ## Deep Agents' tools in Dex
79
+
80
+ The agent keeps Deep Agents' own tools and behavior. The CLI core adds the
81
+ connector, web fetch, and browser tools on top. The agent runs on the
82
+ LangGraph adapter, so every model call is a `RunModel` Step and every tool
83
+ call runs in its own tool Step, after approval when its policy asks for it.
84
+
85
+ | Deep Agents tool | In this command |
86
+ | --- | --- |
87
+ | `ls`, `read_file`, `glob`, `grep` | Reads of the real workspace files. They run at once. |
88
+ | `write_file`, `edit_file`, `delete` | Writes of the real workspace files. They run at once unless `approval.always` names the tool. |
89
+ | `execute` | Runs a command with `/bin/sh` in the workspace. It runs at once unless `approval.always` names it; `local_tools.shell.allow` has no effect. |
90
+ | `task` | Runs a subagent inside the call's tool Step. It runs at once unless `approval.always` names any Deep Agents tool, because the subagent may call it without its own approval. |
91
+ | `write_todos` | Updates the todo list in the agent's state. It runs at once. |
92
+
93
+ - Deep Agents asks for no approval unless `interrupt_on` names a tool, and
94
+ this command follows it: `approval.always` plays the role of
95
+ `interrupt_on`. Connector writes always wait for approval.
96
+ - The file tools use Deep Agents' `LocalShellBackend` on the host's paths,
97
+ rooted at the workspace, so a file tool and `execute` name a file the same
98
+ way. Paths are absolute. A path that resolves outside the workspace fails
99
+ with an error the model sees, and `ls`, `glob`, and `grep` leave out
100
+ entries whose real path is outside it, such as a link to a file elsewhere;
101
+ `grep` never opens such a file.
102
+ - That check is a guardrail against mistaken paths, not a sandbox.
103
+ `execute` runs any command with your permissions, without approval by
104
+ default. A link that changes during a call, or a hard link in the
105
+ workspace to a file elsewhere, is not detected. Use OS-level isolation,
106
+ such as a container, when the agent must not reach other files.
107
+ - `local_tools.files` and `local_tools.shell.enabled` switch the tools on and
108
+ off per turn. `task` needs both, because a subagent uses both.
109
+ - Each tool Step attempt runs Deep Agents' tool middleware around the tool,
110
+ such as `FilesystemMiddleware`'s eviction of a large result to a file. A
111
+ retried attempt runs it again. The
112
+ session Flow sets the adapter's `should_defer_tool_calls_before_middleware`
113
+ for that. Deep Agents writes evicted results and long user messages under
114
+ the paths `/large_tool_results/` and `/conversation_history/`, which its
115
+ file tools read. Each session keeps them in its own directory,
116
+ `~/.dex/agents/deepagents-artifacts/<SHA-256 of the session ID>/`, so two
117
+ sessions never see each other's files and a restarted Worker on the same
118
+ machine finds them. Before each file operation the command checks every
119
+ directory from `deepagents-artifacts` down: you own it, it is no link, and
120
+ a looser mode becomes 0700. `~/.dex` and `~/.dex/agents` keep their mode
121
+ but must not be another user's link. A failed check is an error result of
122
+ that tool call.
123
+ - When the command starts, it deletes the artifacts of every session whose
124
+ files changed more than 30 days ago. It never follows a link, and it logs
125
+ each directory it removes or cannot remove. A session resumed after that
126
+ can no longer read its earlier large results.
127
+ - `edit_file`, `execute`, and `task` calls are never retried, so a Worker that
128
+ dies during one records `outcome_unknown`. `write_file` and `delete` calls
129
+ may run again after a failure.
130
+ - `write_todos` is LangChain's `TodoListMiddleware` tool. It returns a graph
131
+ `Command` that the adapter records and applies. Each turn starts from the
132
+ stored messages, so the todo list lasts for one turn; the plan stays in the
133
+ conversation.
134
+ - `task` runs the subagent's model and tool calls inline, in one tool Step.
135
+ They have no Steps or approvals of their own, use the configuration file's
136
+ model even when session settings change it, and do not count in the
137
+ session's usage. Connector tools are not offered to a subagent.
138
+ - A connector tool whose name a Deep Agents tool has gets the prefix
139
+ `connector_`, such as `connector_read_file`. The other connector tools keep
140
+ their names.
141
+ - Every model call gets the turn's instructions, the workspace path, and the
142
+ turn's model through the adapter's `select_turn_chat_model`.
143
+ - Deep Agents' summarization is turned off for now. It would make an extra
144
+ model call with the configuration file's model inside a `RunModel` Step, so
145
+ a model from the session settings would not reach it. The session does not
146
+ compact either: Dex compaction needs
147
+ the model's context window, which the configuration does not set. A long
148
+ session ends when its input no longer fits the model's context window;
149
+ start a new session then.
150
+
151
+ ## Images
152
+
153
+ `/image <path>` attaches an image file to the next message. The command gives
154
+ the LangChain adapter the media store of `media.store`, so the model sees the
155
+ image, and images that tools return go to the same store. See
156
+ [Images](../../README.md#images).
157
+
158
+ ## Development
159
+
160
+ Develop with uv 0.12.2. The package resolves `dex-agents`, the LangGraph
161
+ adapter, the CLI core, and the test support package from this repository by
162
+ path. `ci.sh` installs the locked dependencies, checks that LangChain and
163
+ LangGraph match the adapter's lock, checks lint and types, and runs the tests
164
+ against a private `dexcli dev`:
165
+
166
+ ```bash
167
+ cd agents/cli/python/deepagents
168
+ DEXCLI=/path/to/dexcli ./ci.sh
169
+ ```
170
+
171
+ The tests run the command in a pseudo-terminal with a private home. The chat
172
+ model calls the scripted Responses API of
173
+ [`dex-agents-cli-testing`](../testing) on 127.0.0.1; a refused proxy stops
174
+ any other request, and no provider variable of the host reaches the command.
175
+ A fake connectors MCP server serves a connector read and write. The tests
176
+ cover a chat turn and `--resume`; `read_file`, `write_file`, and `edit_file` on
177
+ real workspace files without approval; `execute` in the workspace; a path
178
+ outside the workspace, which is refused; `write_file` waiting for approval
179
+ when `approval.always` names it, and rejected file and connector writes that
180
+ never run; a connector read and an approved connector write that runs once; a
181
+ connector tool named `read_file`, which the model sees as
182
+ `connector_connector_read_file` beside a connector tool named
183
+ `connector_read_file`; `write_todos`; a `task` subagent that writes a
184
+ workspace file inside one tool Step, and a `task` that waits for approval
185
+ when `approval.always` names `read_file`; and a large `execute` result that
186
+ lands in the session's own artifacts directory. Backend tests cover links to
187
+ files outside the workspace, which `ls`, `glob`, and `grep` leave out with
188
+ and without ripgrep, which the Python search never opens, and which reads and
189
+ writes refuse; two sessions that write the same large-result path without
190
+ seeing each other's file; a session directory that is a link, which is an
191
+ error result; looser artifact directories, which become 0700; and the
192
+ removal of a session's artifacts after 30 days, which never follows a link.
193
+ They also cover the offered tools, which are Deep Agents' own and none of
194
+ the CLI core's file and shell tools; switched-off file and shell tools, which
195
+ are not offered; a model change and a `system_prompt` in the session
196
+ settings, which reach the next model request; and a key in the `deepagents`
197
+ section. The command also checks that its graph has no summarization, that
198
+ its tool rules cover every tool of the graph, and that a model from the
199
+ session settings reaches every model call.
@@ -0,0 +1,11 @@
1
+ dex_agents_deepagents_cli/__init__.py,sha256=ZVSn5vyc1EvFoAuD5LCWzEZ0BlvKPq5fizm1rsgHQJU,3303
2
+ dex_agents_deepagents_cli/__main__.py,sha256=uSDfXwYWxHJwhAj7UCtHJBTQxnGXne8Y36zGIp7tvuM,439
3
+ dex_agents_deepagents_cli/connector_tool_names.py,sha256=-yk6GznBfoep-tVOpkB4SCwP11LFdosUxacsMyP_92s,5727
4
+ dex_agents_deepagents_cli/harness.py,sha256=QQVth1mZDVn_xgFoxegUgvLdSN0AFJRPePdDYY2S6Ic,17073
5
+ dex_agents_deepagents_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ dex_agents_deepagents_cli/workspace_backend.py,sha256=oLfXfAonzJTt7zsW7631WP9iAs1C9zkyUL64IwmrhzQ,15152
7
+ dex_agents_deepagents_cli-0.6.0.dist-info/licenses/LICENSE,sha256=0AJCV4ky-vrSnT08IIdYpyiFPPoF2UrtKsBAvuvt1G4,4220
8
+ dex_agents_deepagents_cli-0.6.0.dist-info/WHEEL,sha256=q9Wah2_CgP24mRNSlTyb0F8Yrbz55SzVl8eHLkZ_wvE,80
9
+ dex_agents_deepagents_cli-0.6.0.dist-info/entry_points.txt,sha256=mfNobIu3HGTTaMsMjsFyvQpLzpZWev5Oi3_7AjgJS4U,73
10
+ dex_agents_deepagents_cli-0.6.0.dist-info/METADATA,sha256=kwCBKYJsz3Y4MAU0qAq17QgLJcVdQKb24FzkktJiPwg,10950
11
+ dex_agents_deepagents_cli-0.6.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.2
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ dex-agents-deepagents = dex_agents_deepagents_cli:run
3
+