dex-agents-cli-core 0.5.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_cli_core/__init__.py +101 -0
- dex_agents_cli_core/chat.py +538 -0
- dex_agents_cli_core/cli.py +671 -0
- dex_agents_cli_core/config.py +650 -0
- dex_agents_cli_core/config_file.py +210 -0
- dex_agents_cli_core/connectors.py +531 -0
- dex_agents_cli_core/dex_server.py +345 -0
- dex_agents_cli_core/dexcli_install.py +314 -0
- dex_agents_cli_core/environment.py +136 -0
- dex_agents_cli_core/harness.py +445 -0
- dex_agents_cli_core/local_tools/__init__.py +26 -0
- dex_agents_cli_core/local_tools/browser.py +235 -0
- dex_agents_cli_core/local_tools/files.py +512 -0
- dex_agents_cli_core/local_tools/shell.py +85 -0
- dex_agents_cli_core/local_tools/source.py +458 -0
- dex_agents_cli_core/local_tools/web.py +98 -0
- dex_agents_cli_core/py.typed +0 -0
- dex_agents_cli_core/schemas/__init__.py +9 -0
- dex_agents_cli_core/schemas/config.schema.json +315 -0
- dex_agents_cli_core/schemas/session-settings.schema.json +153 -0
- dex_agents_cli_core/tool_policy.py +179 -0
- dex_agents_cli_core/turns.py +235 -0
- dex_agents_cli_core-0.5.0.dist-info/METADATA +479 -0
- dex_agents_cli_core-0.5.0.dist-info/RECORD +26 -0
- dex_agents_cli_core-0.5.0.dist-info/WHEEL +4 -0
- dex_agents_cli_core-0.5.0.dist-info/licenses/LICENSE +105 -0
|
@@ -0,0 +1,235 @@
|
|
|
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
|
+
"""Each turn's configuration: the file's, narrowed by the session settings."""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import dataclasses
|
|
14
|
+
import json
|
|
15
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
16
|
+
from contextvars import ContextVar
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import Any, cast
|
|
19
|
+
|
|
20
|
+
from dex_agents import (
|
|
21
|
+
TURN_SESSION_SETTINGS,
|
|
22
|
+
AgentSessionFlow,
|
|
23
|
+
AgentTurnContext,
|
|
24
|
+
AgentTurnOutcome,
|
|
25
|
+
DeferredToolExecution,
|
|
26
|
+
)
|
|
27
|
+
from dex_agents_cli_core.config import EffectiveConfig, resolve_turn_config
|
|
28
|
+
|
|
29
|
+
_CURRENT_TURN: ContextVar[ResolvedTurn] = ContextVar("dex_agents_cli_turn")
|
|
30
|
+
# Optional runner members the session finds with isinstance checks; a wrapper must keep them.
|
|
31
|
+
_OPTIONAL_RUNNER_METHODS = (
|
|
32
|
+
"build_unfinished_turn_messages",
|
|
33
|
+
"build_user_message_with_images",
|
|
34
|
+
"tool_call_step_options",
|
|
35
|
+
"poll_tool_task",
|
|
36
|
+
"cancel_tool_task",
|
|
37
|
+
"estimate_input_tokens",
|
|
38
|
+
"select_compaction_cutoff",
|
|
39
|
+
"summarize_context",
|
|
40
|
+
)
|
|
41
|
+
_RESOLUTION_CACHE_SIZE = 64
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class ResolvedTurn(EffectiveConfig):
|
|
46
|
+
"""One turn's configuration, narrowed by its session settings, with its instructions.
|
|
47
|
+
|
|
48
|
+
Attributes:
|
|
49
|
+
instructions: ``system_prompt``, else the text of ``system_prompt_file``,
|
|
50
|
+
else ``None`` for the harness's own default instructions.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
instructions: str | None = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TurnConfigResolver:
|
|
57
|
+
"""Resolve each turn's configuration from the file's and the turn's session settings.
|
|
58
|
+
|
|
59
|
+
Resolutions are cached by the settings' JSON text, so the ignored keys of
|
|
60
|
+
one set of settings are reported once.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
file_config: EffectiveConfig,
|
|
66
|
+
file_instructions: str | None,
|
|
67
|
+
report_warnings: Callable[[Sequence[str]], None],
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Bound every turn by the file's configuration.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
file_config: The file's effective configuration.
|
|
73
|
+
file_instructions: The file's instructions: ``system_prompt``, else
|
|
74
|
+
the text of ``system_prompt_file``, else ``None``.
|
|
75
|
+
report_warnings: Called once per distinct settings with the JSON
|
|
76
|
+
Pointers of their ignored keys.
|
|
77
|
+
"""
|
|
78
|
+
self._file_config = file_config
|
|
79
|
+
self._file_instructions = file_instructions
|
|
80
|
+
self._report_warnings = report_warnings
|
|
81
|
+
self._cache: dict[str, ResolvedTurn] = {}
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def file_config(self) -> EffectiveConfig:
|
|
85
|
+
"""Return the file's configuration, which bounds every turn."""
|
|
86
|
+
return self._file_config
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def file_instructions(self) -> str | None:
|
|
90
|
+
"""Return the file's instructions."""
|
|
91
|
+
return self._file_instructions
|
|
92
|
+
|
|
93
|
+
def resolve(self, session_settings: Mapping[str, Any] | None) -> ResolvedTurn:
|
|
94
|
+
"""Return the turn configuration for some session settings.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
session_settings: The turn's settings; ``None`` or ``{}`` for none.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
The turn's configuration and instructions.
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
ConfigError: If the settings fail the schema or widen the file. Its
|
|
104
|
+
message is ``<code> at "<pointer>"``.
|
|
105
|
+
"""
|
|
106
|
+
settings = dict(session_settings) if session_settings else None
|
|
107
|
+
key = json.dumps(settings, sort_keys=True)
|
|
108
|
+
cached = self._cache.get(key)
|
|
109
|
+
if cached is not None:
|
|
110
|
+
return cached
|
|
111
|
+
resolution = resolve_turn_config(self._file_config, settings)
|
|
112
|
+
if resolution.warnings:
|
|
113
|
+
self._report_warnings(resolution.warnings)
|
|
114
|
+
turn = resolution.config
|
|
115
|
+
instructions = turn.system_prompt
|
|
116
|
+
if instructions is None and turn.system_prompt_file is not None:
|
|
117
|
+
instructions = self._file_instructions
|
|
118
|
+
resolved = ResolvedTurn(
|
|
119
|
+
**{field.name: getattr(turn, field.name) for field in dataclasses.fields(turn)},
|
|
120
|
+
instructions=instructions,
|
|
121
|
+
)
|
|
122
|
+
if len(self._cache) >= _RESOLUTION_CACHE_SIZE:
|
|
123
|
+
self._cache.clear()
|
|
124
|
+
self._cache[key] = resolved
|
|
125
|
+
return resolved
|
|
126
|
+
|
|
127
|
+
def resolve_turn(self, turn: AgentTurnContext) -> ResolvedTurn:
|
|
128
|
+
"""Return the configuration of the turn a ``RunModel`` Step runs.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
turn: The model call's context.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
The turn's configuration.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
ConfigError: If the turn's settings are rejected.
|
|
138
|
+
"""
|
|
139
|
+
return self.resolve(turn.session_settings)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def current_turn_config() -> ResolvedTurn | None:
|
|
143
|
+
"""Return the configuration of the turn whose model call or tool Step is running in this task.
|
|
144
|
+
|
|
145
|
+
The CLI core sets it around every ``run_model_step``, from the turn's
|
|
146
|
+
session settings, and around every ``execute_deferred_tool_call``, from
|
|
147
|
+
the settings the session recorded for the call's turn. Tasks that these
|
|
148
|
+
start inherit it, such as tool source listings.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
The turn's configuration, or ``None`` outside a model call and a tool Step.
|
|
152
|
+
"""
|
|
153
|
+
return _CURRENT_TURN.get(None)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def with_turn_config(flow: AgentSessionFlow, resolver: TurnConfigResolver) -> AgentSessionFlow:
|
|
157
|
+
"""Return the same agent session Flow, with each turn's configuration resolved first.
|
|
158
|
+
|
|
159
|
+
Every ``run_model_step`` resolves the turn's session settings, which fails
|
|
160
|
+
the model call with ``ConfigError`` when they are rejected, and runs the
|
|
161
|
+
harness's runner with ``current_turn_config`` set.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
flow: The Flow a harness built.
|
|
165
|
+
resolver: The resolver of the command's configuration file.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
A new Flow with the same type and options around the wrapped runner.
|
|
169
|
+
"""
|
|
170
|
+
runner = _wrap_runner(flow.runner, resolver)
|
|
171
|
+
return AgentSessionFlow(runner, flow_type=flow.get_flow_type(), options=flow.options)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class _TurnConfiguredRunner:
|
|
175
|
+
def __init__(self, inner: Any, resolver: TurnConfigResolver) -> None:
|
|
176
|
+
self._inner = inner
|
|
177
|
+
self._resolver = resolver
|
|
178
|
+
|
|
179
|
+
def build_user_message(self, text: str) -> Any:
|
|
180
|
+
return self._inner.build_user_message(text)
|
|
181
|
+
|
|
182
|
+
async def run_model_step(self, turn: AgentTurnContext) -> AgentTurnOutcome:
|
|
183
|
+
token = _CURRENT_TURN.set(self._resolver.resolve_turn(turn))
|
|
184
|
+
try:
|
|
185
|
+
outcome: AgentTurnOutcome = await self._inner.run_model_step(turn)
|
|
186
|
+
finally:
|
|
187
|
+
_CURRENT_TURN.reset(token)
|
|
188
|
+
return outcome
|
|
189
|
+
|
|
190
|
+
async def execute_deferred_tool_call(self, call: DeferredToolExecution) -> Any:
|
|
191
|
+
# A tool Step runs with its turn's configuration too, so replacement tools see it.
|
|
192
|
+
token = _CURRENT_TURN.set(self._resolver.resolve(recorded_turn_settings(call)))
|
|
193
|
+
try:
|
|
194
|
+
return await self._inner.execute_deferred_tool_call(call)
|
|
195
|
+
finally:
|
|
196
|
+
_CURRENT_TURN.reset(token)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def recorded_turn_settings(call: DeferredToolExecution) -> Mapping[str, Any]:
|
|
200
|
+
"""Return the session settings a tool call's turn started with, as the session recorded them.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
call: The tool Step's call.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
The turn's settings; ``{}`` when the turn started without settings or
|
|
207
|
+
the Step has no context.
|
|
208
|
+
"""
|
|
209
|
+
if call.context is None:
|
|
210
|
+
return {}
|
|
211
|
+
stored = cast(Any, TURN_SESSION_SETTINGS).get(call.context)
|
|
212
|
+
if stored is None or stored.turn_id != call.tool_call.turn_id:
|
|
213
|
+
return {}
|
|
214
|
+
return cast(Mapping[str, Any], stored.session_settings)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _wrap_runner(inner: Any, resolver: TurnConfigResolver) -> Any:
|
|
218
|
+
"""Build a wrapper class with exactly the optional members the inner runner has."""
|
|
219
|
+
namespace: dict[str, Any] = {
|
|
220
|
+
name: _forwarding_method(name)
|
|
221
|
+
for name in _OPTIONAL_RUNNER_METHODS
|
|
222
|
+
if callable(getattr(inner, name, None))
|
|
223
|
+
}
|
|
224
|
+
if hasattr(inner, "supports_plan_mode"):
|
|
225
|
+
namespace["supports_plan_mode"] = property(lambda self: self._inner.supports_plan_mode)
|
|
226
|
+
wrapper_type = type("TurnConfiguredRunner", (_TurnConfiguredRunner,), namespace)
|
|
227
|
+
return wrapper_type(inner, resolver)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _forwarding_method(name: str) -> Callable[..., Any]:
|
|
231
|
+
def forward(self: _TurnConfiguredRunner, *args: Any, **kwargs: Any) -> Any:
|
|
232
|
+
return getattr(self._inner, name)(*args, **kwargs)
|
|
233
|
+
|
|
234
|
+
forward.__name__ = name
|
|
235
|
+
return forward
|