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.
- dex_agents_deepagents_cli/__init__.py +112 -0
- dex_agents_deepagents_cli/__main__.py +14 -0
- dex_agents_deepagents_cli/connector_tool_names.py +138 -0
- dex_agents_deepagents_cli/harness.py +443 -0
- dex_agents_deepagents_cli/py.typed +0 -0
- dex_agents_deepagents_cli/workspace_backend.py +382 -0
- dex_agents_deepagents_cli-0.6.0.dist-info/METADATA +199 -0
- dex_agents_deepagents_cli-0.6.0.dist-info/RECORD +11 -0
- dex_agents_deepagents_cli-0.6.0.dist-info/WHEEL +4 -0
- dex_agents_deepagents_cli-0.6.0.dist-info/entry_points.txt +3 -0
- dex_agents_deepagents_cli-0.6.0.dist-info/licenses/LICENSE +105 -0
|
@@ -0,0 +1,443 @@
|
|
|
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 harness: a ``create_deep_agent`` agent on the Dex LangGraph adapter."""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import dataclasses
|
|
14
|
+
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import dex_agents_cli_core # noqa: F401 # first: it configures gRPC before anything imports it
|
|
20
|
+
|
|
21
|
+
# isort: split
|
|
22
|
+
|
|
23
|
+
from deepagents import create_deep_agent
|
|
24
|
+
from langchain.agents.middleware import (
|
|
25
|
+
AgentMiddleware,
|
|
26
|
+
ModelRequest,
|
|
27
|
+
ModelResponse,
|
|
28
|
+
TodoListMiddleware,
|
|
29
|
+
)
|
|
30
|
+
from langchain.chat_models import init_chat_model
|
|
31
|
+
from langchain_core.language_models import BaseChatModel
|
|
32
|
+
from langchain_core.messages import SystemMessage
|
|
33
|
+
from langchain_core.tools import BaseTool
|
|
34
|
+
|
|
35
|
+
from dex_agents import (
|
|
36
|
+
AgentSessionFlow,
|
|
37
|
+
AgentSessionOptions,
|
|
38
|
+
AgentTurnContext,
|
|
39
|
+
AgentTurnOutcome,
|
|
40
|
+
DeferredToolCall,
|
|
41
|
+
DeferredToolExecution,
|
|
42
|
+
DeferredToolOutcome,
|
|
43
|
+
ToolCallsRequested,
|
|
44
|
+
ToolEffect,
|
|
45
|
+
ToolExecutionError,
|
|
46
|
+
ToolPolicy,
|
|
47
|
+
)
|
|
48
|
+
from dex_agents.builtin_loop import ToolSource
|
|
49
|
+
from dex_agents_cli_core import (
|
|
50
|
+
CONNECTORS_TOOL_SOURCE_NAME,
|
|
51
|
+
ConfigError,
|
|
52
|
+
EffectiveConfig,
|
|
53
|
+
HarnessContext,
|
|
54
|
+
LocalToolGroup,
|
|
55
|
+
current_turn_config,
|
|
56
|
+
is_local_tool_group_enabled,
|
|
57
|
+
resolve_turn_tool_policy,
|
|
58
|
+
)
|
|
59
|
+
from dex_agents_deepagents_cli.connector_tool_names import PrefixedConnectorToolSource
|
|
60
|
+
from dex_agents_deepagents_cli.workspace_backend import (
|
|
61
|
+
bind_artifacts_session,
|
|
62
|
+
build_workspace_backend,
|
|
63
|
+
default_artifacts_directory,
|
|
64
|
+
remove_stale_session_artifacts,
|
|
65
|
+
)
|
|
66
|
+
from dex_agents_langgraph import (
|
|
67
|
+
CompiledGraphSource,
|
|
68
|
+
DurableToolCallMiddleware,
|
|
69
|
+
LangGraphAgentRunner,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
#: The Flow type of the command's sessions, before the CLI core namespaces it.
|
|
73
|
+
DEEPAGENTS_CLI_FLOW_TYPE = "DexAgentsDeepAgentsCliSession"
|
|
74
|
+
|
|
75
|
+
#: The model the command uses when the configuration file sets none.
|
|
76
|
+
DEFAULT_DEEPAGENTS_MODEL = "openai:gpt-5.2"
|
|
77
|
+
|
|
78
|
+
#: The instructions the agent gets when the configuration sets none.
|
|
79
|
+
DEFAULT_INSTRUCTIONS = (
|
|
80
|
+
"You are a helpful assistant. Use your tools to work with the files and commands "
|
|
81
|
+
"in your workspace and with the connected services. Plan multi-step work with "
|
|
82
|
+
"write_todos."
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
#: The todo tool's name, as Deep Agents and LangChain name it.
|
|
86
|
+
WRITE_TODOS_TOOL_NAME = "write_todos"
|
|
87
|
+
|
|
88
|
+
#: Deep Agents' subagent tool. A subagent runs inside the tool Step of its call.
|
|
89
|
+
TASK_TOOL_NAME = "task"
|
|
90
|
+
|
|
91
|
+
#: Deep Agents' shell tool.
|
|
92
|
+
EXECUTE_TOOL_NAME = "execute"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True)
|
|
96
|
+
class DeepAgentsToolRule:
|
|
97
|
+
"""How the command runs one of Deep Agents' own tools.
|
|
98
|
+
|
|
99
|
+
Attributes:
|
|
100
|
+
effect: The effect class of each call, which decides its retries.
|
|
101
|
+
groups: The local tool groups the turn must switch on, or the tool
|
|
102
|
+
is neither offered nor run.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
effect: ToolEffect
|
|
106
|
+
groups: frozenset[LocalToolGroup] = frozenset()
|
|
107
|
+
|
|
108
|
+
def is_enabled(self, turn: EffectiveConfig) -> bool:
|
|
109
|
+
"""Report whether the turn switches on every group the tool needs."""
|
|
110
|
+
return all(is_local_tool_group_enabled(turn, group) for group in self.groups)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
_FILES = frozenset({LocalToolGroup.FILES})
|
|
114
|
+
|
|
115
|
+
#: The rule of every tool the agent's graph has. No tool waits for approval
|
|
116
|
+
#: unless ``approval.always`` names it, as in Deep Agents without ``interrupt_on``.
|
|
117
|
+
DEEP_AGENTS_TOOL_RULES: Mapping[str, DeepAgentsToolRule] = {
|
|
118
|
+
"ls": DeepAgentsToolRule(ToolEffect.READ, _FILES),
|
|
119
|
+
"read_file": DeepAgentsToolRule(ToolEffect.READ, _FILES),
|
|
120
|
+
"glob": DeepAgentsToolRule(ToolEffect.READ, _FILES),
|
|
121
|
+
"grep": DeepAgentsToolRule(ToolEffect.READ, _FILES),
|
|
122
|
+
"write_file": DeepAgentsToolRule(ToolEffect.IDEMPOTENT_WRITE, _FILES),
|
|
123
|
+
# A rerun of a replace_all edit can apply it twice.
|
|
124
|
+
"edit_file": DeepAgentsToolRule(ToolEffect.NON_IDEMPOTENT_WRITE, _FILES),
|
|
125
|
+
"delete": DeepAgentsToolRule(ToolEffect.IDEMPOTENT_WRITE, _FILES),
|
|
126
|
+
EXECUTE_TOOL_NAME: DeepAgentsToolRule(
|
|
127
|
+
ToolEffect.NON_IDEMPOTENT_WRITE, frozenset({LocalToolGroup.SHELL})
|
|
128
|
+
),
|
|
129
|
+
# A subagent may use every file tool and execute, without their own Steps or approvals.
|
|
130
|
+
TASK_TOOL_NAME: DeepAgentsToolRule(
|
|
131
|
+
ToolEffect.NON_IDEMPOTENT_WRITE, frozenset({LocalToolGroup.FILES, LocalToolGroup.SHELL})
|
|
132
|
+
),
|
|
133
|
+
WRITE_TODOS_TOOL_NAME: DeepAgentsToolRule(ToolEffect.READ),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#: Tools whose ``approval.always`` entry makes ``task`` wait too: a subagent calls them unseen.
|
|
137
|
+
_TASK_APPROVAL_TOOL_NAMES = frozenset(DEEP_AGENTS_TOOL_RULES)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class DeepAgentsHarness:
|
|
141
|
+
"""The ``dex-agents-deepagents`` command: a Deep Agents agent in the terminal.
|
|
142
|
+
|
|
143
|
+
The agent comes from ``create_deep_agent`` with Deep Agents' own tools:
|
|
144
|
+
the file tools and ``execute`` on the workspace, ``write_todos``, and the
|
|
145
|
+
``task`` subagent tool. The CLI core's connector, web fetch, and browser
|
|
146
|
+
tools come on top. The agent runs on the Dex LangGraph adapter, so every
|
|
147
|
+
model call is a ``RunModel`` Step and every tool call its own tool Step,
|
|
148
|
+
which runs Deep Agents' tool middleware once. As in Deep Agents without
|
|
149
|
+
``interrupt_on``, its own tools run without approval unless
|
|
150
|
+
``approval.always`` names them; connector writes always wait for one.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
name = "deepagents"
|
|
154
|
+
config_section = "deepagents"
|
|
155
|
+
flow_type = DEEPAGENTS_CLI_FLOW_TYPE
|
|
156
|
+
default_model: str | None = DEFAULT_DEEPAGENTS_MODEL
|
|
157
|
+
replaced_local_tools: frozenset[LocalToolGroup] = frozenset(
|
|
158
|
+
{LocalToolGroup.FILES, LocalToolGroup.SHELL}
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def create_session_flow(self, context: HarnessContext) -> AgentSessionFlow:
|
|
162
|
+
"""Build the Deep Agents session Flow from the effective configuration.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
context: The configuration, workspace, and tool sources.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
The Flow, with ``context.flow_type`` as its type.
|
|
169
|
+
|
|
170
|
+
It first deletes the artifacts of sessions that changed none of
|
|
171
|
+
them within ``SESSION_ARTIFACTS_RETENTION``.
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
ConfigError: If the ``deepagents`` section has a key; it has no options yet.
|
|
175
|
+
"""
|
|
176
|
+
validate_deepagents_section(context.harness_options)
|
|
177
|
+
remove_stale_session_artifacts(default_artifacts_directory())
|
|
178
|
+
file_model = context.config.model
|
|
179
|
+
workspace = context.workspace
|
|
180
|
+
runner = DeepAgentsAgentRunner(
|
|
181
|
+
context,
|
|
182
|
+
lambda: build_deep_agent(file_model, workspace),
|
|
183
|
+
tool_sources=deep_agents_tool_sources(context.tool_sources),
|
|
184
|
+
select_turn_chat_model=TurnChatModelSelector(file_model),
|
|
185
|
+
)
|
|
186
|
+
return AgentSessionFlow(
|
|
187
|
+
runner,
|
|
188
|
+
flow_type=context.flow_type,
|
|
189
|
+
options=AgentSessionOptions(
|
|
190
|
+
summary_attributes_indexed=context.is_session_summary_indexed
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def build_deep_agent(file_model: str, workspace: Path) -> Any:
|
|
196
|
+
"""Build the command's Deep Agents graph.
|
|
197
|
+
|
|
198
|
+
Deep Agents' file tools and ``execute`` work on the workspace through
|
|
199
|
+
``build_workspace_backend``. LangChain's ``TodoListMiddleware`` adds
|
|
200
|
+
``write_todos``. The summarization is replaced by middleware that adds
|
|
201
|
+
nothing: it would make an extra model call with the file's model inside a
|
|
202
|
+
``RunModel`` Step.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
file_model: The configuration file's model; each turn may override it.
|
|
206
|
+
A subagent of ``task`` always uses it.
|
|
207
|
+
workspace: The absolute workspace directory.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
The compiled agent graph.
|
|
211
|
+
"""
|
|
212
|
+
middleware: list[AgentMiddleware[Any, Any, Any]] = [
|
|
213
|
+
TodoListMiddleware(),
|
|
214
|
+
DurableToolCallMiddleware(),
|
|
215
|
+
TurnInstructionsMiddleware(workspace),
|
|
216
|
+
LocalToolSwitchesMiddleware(),
|
|
217
|
+
_WithoutSummarization(),
|
|
218
|
+
]
|
|
219
|
+
return create_deep_agent(
|
|
220
|
+
deep_agents_chat_model(file_model),
|
|
221
|
+
backend=build_workspace_backend(workspace),
|
|
222
|
+
middleware=middleware,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def deep_agents_tool_sources(tool_sources: Sequence[ToolSource]) -> list[ToolSource]:
|
|
227
|
+
"""Return the CLI core's tool sources, with connector tools that fit beside Deep Agents'.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
tool_sources: ``HarnessContext.tool_sources``.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
The sources; the connectors source prefixes a tool whose name a Deep
|
|
234
|
+
Agents tool uses and asks approval for every write.
|
|
235
|
+
"""
|
|
236
|
+
return [
|
|
237
|
+
PrefixedConnectorToolSource(source, DEEP_AGENTS_TOOL_RULES.keys())
|
|
238
|
+
if source.source_name == CONNECTORS_TOOL_SOURCE_NAME
|
|
239
|
+
else source
|
|
240
|
+
for source in tool_sources
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class DeepAgentsAgentRunner(LangGraphAgentRunner):
|
|
245
|
+
"""The LangGraph runner with the CLI core's per-turn rules for Deep Agents' own tools.
|
|
246
|
+
|
|
247
|
+
Each call of a Deep Agents tool gets its effect from
|
|
248
|
+
``DEEP_AGENTS_TOOL_RULES`` and waits for approval only when the turn's
|
|
249
|
+
``approval.always`` names it. A call of a tool whose local tool group the
|
|
250
|
+
turn switches off runs at once and its tool Step refuses it.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
def __init__(
|
|
254
|
+
self,
|
|
255
|
+
context: HarnessContext,
|
|
256
|
+
graph: CompiledGraphSource,
|
|
257
|
+
*,
|
|
258
|
+
tool_sources: Sequence[ToolSource],
|
|
259
|
+
select_turn_chat_model: Callable[[AgentTurnContext], BaseChatModel | None],
|
|
260
|
+
) -> None:
|
|
261
|
+
"""Create the runner, deferring every tool call before Deep Agents' middleware.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
context: The harness context, for each turn's configuration.
|
|
265
|
+
graph: The graph, or the function that builds it on the Worker.
|
|
266
|
+
tool_sources: The connector and local tool sources.
|
|
267
|
+
select_turn_chat_model: Picks each turn's chat model.
|
|
268
|
+
"""
|
|
269
|
+
super().__init__(
|
|
270
|
+
graph,
|
|
271
|
+
tool_sources=tool_sources,
|
|
272
|
+
media_store=context.media_store,
|
|
273
|
+
select_turn_chat_model=select_turn_chat_model,
|
|
274
|
+
should_defer_tool_calls_before_middleware=True,
|
|
275
|
+
)
|
|
276
|
+
self._context = context
|
|
277
|
+
|
|
278
|
+
async def run_model_step(self, turn: AgentTurnContext) -> AgentTurnOutcome:
|
|
279
|
+
"""Run one model call and give each Deep Agents tool call the turn's policy."""
|
|
280
|
+
with bind_artifacts_session(turn.flow_id):
|
|
281
|
+
outcome = await super().run_model_step(turn)
|
|
282
|
+
if not isinstance(outcome, ToolCallsRequested):
|
|
283
|
+
return outcome
|
|
284
|
+
turn_config = self._context.resolve_turn(turn.session_settings)
|
|
285
|
+
return dataclasses.replace(
|
|
286
|
+
outcome,
|
|
287
|
+
tool_calls=[
|
|
288
|
+
_with_turn_policy(turn, turn_config, tool_call) for tool_call in outcome.tool_calls
|
|
289
|
+
],
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
async def execute_deferred_tool_call(self, call: DeferredToolExecution) -> DeferredToolOutcome:
|
|
293
|
+
"""Run one call, after refusing a Deep Agents tool its turn switches off here.
|
|
294
|
+
|
|
295
|
+
Raises:
|
|
296
|
+
ToolExecutionError: The call's turn switches the tool's local tool
|
|
297
|
+
group off on this Worker, so nothing runs.
|
|
298
|
+
"""
|
|
299
|
+
rule = DEEP_AGENTS_TOOL_RULES.get(call.tool_call.tool_name)
|
|
300
|
+
if rule is not None and not rule.is_enabled(
|
|
301
|
+
self._context.resolve_tool_step_permissions(call)
|
|
302
|
+
):
|
|
303
|
+
raise ToolExecutionError(
|
|
304
|
+
f"tool {call.tool_call.tool_name} is switched off for this session"
|
|
305
|
+
)
|
|
306
|
+
with bind_artifacts_session(call.flow_id):
|
|
307
|
+
return await super().execute_deferred_tool_call(call)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _with_turn_policy(
|
|
311
|
+
turn: AgentTurnContext, turn_config: EffectiveConfig, tool_call: DeferredToolCall
|
|
312
|
+
) -> DeferredToolCall:
|
|
313
|
+
# Sources rename colliding tools, so a rule's name is always a graph tool.
|
|
314
|
+
rule = DEEP_AGENTS_TOOL_RULES.get(tool_call.tool_name)
|
|
315
|
+
if rule is None:
|
|
316
|
+
return tool_call
|
|
317
|
+
if not rule.is_enabled(turn_config):
|
|
318
|
+
# Its tool Step only records the refusal, so nothing waits for approval.
|
|
319
|
+
policy = ToolPolicy(ToolEffect.READ, requires_approval=False)
|
|
320
|
+
else:
|
|
321
|
+
is_task_approval_required = tool_call.tool_name == TASK_TOOL_NAME and bool(
|
|
322
|
+
_TASK_APPROVAL_TOOL_NAMES & set(turn_config.approval.always)
|
|
323
|
+
)
|
|
324
|
+
policy = resolve_turn_tool_policy(
|
|
325
|
+
turn, tool_call.tool_name, rule.effect, is_task_approval_required, turn_config
|
|
326
|
+
)
|
|
327
|
+
return dataclasses.replace(tool_call, policy=policy)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def validate_deepagents_section(options: Mapping[str, Any]) -> None:
|
|
331
|
+
"""Reject every key of the ``deepagents`` section, which has no options yet.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
options: The section, ``{}`` when the file omits it.
|
|
335
|
+
|
|
336
|
+
Raises:
|
|
337
|
+
ConfigError: ``schema`` with the first key's JSON Pointer.
|
|
338
|
+
"""
|
|
339
|
+
for key in sorted(options):
|
|
340
|
+
escaped = key.replace("~", "~0").replace("/", "~1")
|
|
341
|
+
raise ConfigError("schema", f"/deepagents/{escaped}", keyword="additionalProperties")
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def deep_agents_chat_model(model: str) -> BaseChatModel:
|
|
345
|
+
"""Build a chat model from a ``provider:model`` ID as Deep Agents does.
|
|
346
|
+
|
|
347
|
+
An ``openai:`` model uses the Responses API, Deep Agents' default.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
model: The ID, such as ``openai:gpt-5.2`` or ``anthropic:claude-opus-5-5``.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
The chat model. Its provider reads its key from the standard environment variable.
|
|
354
|
+
"""
|
|
355
|
+
if model.startswith("openai:"):
|
|
356
|
+
return init_chat_model(model, use_responses_api=True)
|
|
357
|
+
return init_chat_model(model)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class TurnChatModelSelector:
|
|
361
|
+
"""Pick the running turn's chat model when it differs from the configuration file's."""
|
|
362
|
+
|
|
363
|
+
def __init__(self, file_model: str) -> None:
|
|
364
|
+
"""Keep the model the agent was built with.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
file_model: The configuration file's model.
|
|
368
|
+
"""
|
|
369
|
+
self._file_model = file_model
|
|
370
|
+
self._models: dict[str, BaseChatModel] = {}
|
|
371
|
+
|
|
372
|
+
def __call__(self, turn: AgentTurnContext) -> BaseChatModel | None:
|
|
373
|
+
"""Return the turn's chat model, built once per ID, or ``None`` for the file's model."""
|
|
374
|
+
turn_config = current_turn_config()
|
|
375
|
+
if turn_config is None or turn_config.model == self._file_model:
|
|
376
|
+
return None
|
|
377
|
+
if turn_config.model not in self._models:
|
|
378
|
+
self._models[turn_config.model] = deep_agents_chat_model(turn_config.model)
|
|
379
|
+
return self._models[turn_config.model]
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class TurnInstructionsMiddleware(AgentMiddleware[Any, Any, Any]):
|
|
383
|
+
"""Put the running turn's instructions and the workspace first in every system message."""
|
|
384
|
+
|
|
385
|
+
def __init__(self, workspace: Path | None = None) -> None:
|
|
386
|
+
"""Name the workspace the file tools work in.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
workspace: The absolute workspace directory; ``None`` names none.
|
|
390
|
+
"""
|
|
391
|
+
super().__init__()
|
|
392
|
+
self._workspace = workspace
|
|
393
|
+
|
|
394
|
+
async def awrap_model_call(
|
|
395
|
+
self,
|
|
396
|
+
request: ModelRequest[Any],
|
|
397
|
+
handler: Callable[[ModelRequest[Any]], Awaitable[ModelResponse[Any]]],
|
|
398
|
+
) -> ModelResponse[Any]:
|
|
399
|
+
"""Prepend the turn's instructions, or the default ones, to the system message."""
|
|
400
|
+
turn = current_turn_config()
|
|
401
|
+
if turn is None:
|
|
402
|
+
return await handler(request)
|
|
403
|
+
sections = [turn.instructions or DEFAULT_INSTRUCTIONS]
|
|
404
|
+
if self._workspace is not None:
|
|
405
|
+
sections.append(
|
|
406
|
+
f"Your workspace is {self._workspace}. File tools take absolute paths "
|
|
407
|
+
"inside it, and execute runs commands in it."
|
|
408
|
+
)
|
|
409
|
+
if request.system_message is not None and request.system_message.text:
|
|
410
|
+
sections.append(request.system_message.text)
|
|
411
|
+
return await handler(request.override(system_message=SystemMessage("\n\n".join(sections))))
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class LocalToolSwitchesMiddleware(AgentMiddleware[Any, Any, Any]):
|
|
415
|
+
"""Leave out the Deep Agents tools whose local tool group the running turn switches off."""
|
|
416
|
+
|
|
417
|
+
async def awrap_model_call(
|
|
418
|
+
self,
|
|
419
|
+
request: ModelRequest[Any],
|
|
420
|
+
handler: Callable[[ModelRequest[Any]], Awaitable[ModelResponse[Any]]],
|
|
421
|
+
) -> ModelResponse[Any]:
|
|
422
|
+
"""Remove the switched-off tools from the model request."""
|
|
423
|
+
turn = current_turn_config()
|
|
424
|
+
if turn is None:
|
|
425
|
+
return await handler(request)
|
|
426
|
+
tools = [tool for tool in request.tools if _is_offered(tool, turn)]
|
|
427
|
+
if len(tools) == len(request.tools):
|
|
428
|
+
return await handler(request)
|
|
429
|
+
return await handler(request.override(tools=tools))
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _is_offered(tool: BaseTool | Mapping[str, Any], turn: EffectiveConfig) -> bool:
|
|
433
|
+
name = tool.name if isinstance(tool, BaseTool) else tool.get("name")
|
|
434
|
+
rule = DEEP_AGENTS_TOOL_RULES.get(name) if isinstance(name, str) else None
|
|
435
|
+
return rule is None or rule.is_enabled(turn)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
class _WithoutSummarization(AgentMiddleware[Any, Any, Any]):
|
|
439
|
+
"""Replace the summarization, which calls the file's model inside a ``RunModel`` Step."""
|
|
440
|
+
|
|
441
|
+
@property
|
|
442
|
+
def name(self) -> str:
|
|
443
|
+
return "SummarizationMiddleware"
|
|
File without changes
|