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.
@@ -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