delegate-agent-cli 0.11.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.
- delegate_agent/__init__.py +5 -0
- delegate_agent/archived_logs.py +44 -0
- delegate_agent/argv_builders.py +423 -0
- delegate_agent/argv_utils.py +36 -0
- delegate_agent/capability_commands.py +99 -0
- delegate_agent/cli.py +987 -0
- delegate_agent/cli_parser.py +1627 -0
- delegate_agent/command_errors.py +29 -0
- delegate_agent/command_help.py +1264 -0
- delegate_agent/config.py +1117 -0
- delegate_agent/config_commands.py +143 -0
- delegate_agent/constants.py +50 -0
- delegate_agent/describe_payload.py +1018 -0
- delegate_agent/errors.py +32 -0
- delegate_agent/git_utils.py +180 -0
- delegate_agent/harness_events.py +719 -0
- delegate_agent/inspection_commands.py +104 -0
- delegate_agent/isolation.py +316 -0
- delegate_agent/json_types.py +18 -0
- delegate_agent/log_output.py +78 -0
- delegate_agent/private_io.py +109 -0
- delegate_agent/profile_commands.py +42 -0
- delegate_agent/profile_guard.py +116 -0
- delegate_agent/profiles.py +389 -0
- delegate_agent/prompt_instructions.py +39 -0
- delegate_agent/prompt_transport.py +12 -0
- delegate_agent/reasoning.py +916 -0
- delegate_agent/redaction.py +193 -0
- delegate_agent/rendering.py +573 -0
- delegate_agent/request_build.py +1681 -0
- delegate_agent/request_models.py +191 -0
- delegate_agent/retention.py +321 -0
- delegate_agent/run_metadata.py +78 -0
- delegate_agent/run_output_commands.py +645 -0
- delegate_agent/run_registry.py +747 -0
- delegate_agent/run_status.py +300 -0
- delegate_agent/runner.py +1830 -0
- delegate_agent/safe_workspace.py +821 -0
- delegate_agent/snapshot_view.py +229 -0
- delegate_agent/wait_cancel_commands.py +559 -0
- delegate_agent/worktree_commands.py +218 -0
- delegate_agent/worktree_execution.py +654 -0
- delegate_agent/worktree_gc.py +529 -0
- delegate_agent/worktree_mgmt.py +782 -0
- delegate_agent/worktree_records.py +232 -0
- delegate_agent/worktree_remove.py +547 -0
- delegate_agent/worktree_summary.py +242 -0
- delegate_agent/wsl.py +65 -0
- delegate_agent_cli-0.11.0.dist-info/METADATA +325 -0
- delegate_agent_cli-0.11.0.dist-info/RECORD +54 -0
- delegate_agent_cli-0.11.0.dist-info/WHEEL +5 -0
- delegate_agent_cli-0.11.0.dist-info/entry_points.txt +2 -0
- delegate_agent_cli-0.11.0.dist-info/licenses/LICENSE +21 -0
- delegate_agent_cli-0.11.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,916 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Protocol, TypeAlias, cast
|
|
9
|
+
|
|
10
|
+
from delegate_agent import run_registry
|
|
11
|
+
from delegate_agent.json_types import JsonObject, JsonValue
|
|
12
|
+
|
|
13
|
+
ReasoningDeclaration: TypeAlias = Mapping[str, object]
|
|
14
|
+
|
|
15
|
+
# Bundled capabilities are a conservative fallback only. User config and a
|
|
16
|
+
# refreshed workspace cache take precedence so private/custom models do not
|
|
17
|
+
# require Delegate source changes, and stale bundled data can be bypassed.
|
|
18
|
+
BUNDLED_REASONING_CAPABILITIES: dict[str, dict[str, ReasoningDeclaration]] = {
|
|
19
|
+
"codex": {
|
|
20
|
+
"gpt-5.5": {
|
|
21
|
+
"supported": ("low", "medium", "high", "xhigh"),
|
|
22
|
+
"default": "medium",
|
|
23
|
+
},
|
|
24
|
+
"gpt-5.4": {
|
|
25
|
+
"supported": ("low", "medium", "high", "xhigh"),
|
|
26
|
+
"default": "medium",
|
|
27
|
+
},
|
|
28
|
+
"gpt-5.4-mini": {
|
|
29
|
+
"supported": ("low", "medium", "high", "xhigh"),
|
|
30
|
+
"default": "high",
|
|
31
|
+
},
|
|
32
|
+
"gpt-5.3-codex-spark": {
|
|
33
|
+
"supported": ("low", "medium", "high", "xhigh"),
|
|
34
|
+
"default": "medium",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
"droid": {
|
|
38
|
+
"claude-opus-4-8": {
|
|
39
|
+
"supported": ("off", "low", "medium", "high", "xhigh", "max"),
|
|
40
|
+
"default": "high",
|
|
41
|
+
},
|
|
42
|
+
"claude-sonnet-4-6": {
|
|
43
|
+
"supported": ("off", "low", "medium", "high", "max"),
|
|
44
|
+
"default": "high",
|
|
45
|
+
},
|
|
46
|
+
"gpt-5.5": {
|
|
47
|
+
"supported": ("low", "medium", "high", "xhigh"),
|
|
48
|
+
"default": "medium",
|
|
49
|
+
},
|
|
50
|
+
"gemini-3.5-flash": {
|
|
51
|
+
"supported": ("minimal", "low", "medium", "high"),
|
|
52
|
+
"default": "high",
|
|
53
|
+
},
|
|
54
|
+
"glm-5.1": {"supported": ("off", "high"), "default": "high"},
|
|
55
|
+
"minimax-m2.7": {"supported": ("high",), "default": "high"},
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Argv builders compare a capability's transport against these constants, so
|
|
60
|
+
# they must stay in lockstep with the table below — hence named constants, not
|
|
61
|
+
# re-typed string literals at the emission sites.
|
|
62
|
+
TRANSPORT_CODEX_CONFIG = "codex-config"
|
|
63
|
+
TRANSPORT_DROID_FLAG = "droid-flag"
|
|
64
|
+
TRANSPORT_CURSOR_MODEL_SELECTION = "cursor-model-selection"
|
|
65
|
+
TRANSPORT_CLAUDE_EFFORT_FLAG = "claude-effort-flag"
|
|
66
|
+
CLAUDE_NATIVE_EFFORTS = ("low", "medium", "high", "xhigh", "max")
|
|
67
|
+
TRANSPORT_GROK_EFFORT_FLAG = "grok-effort-flag"
|
|
68
|
+
GROK_NATIVE_EFFORTS = CLAUDE_NATIVE_EFFORTS
|
|
69
|
+
INSPECT_REASONING_DISCOVERY_HINT = (
|
|
70
|
+
"Inspect `delegate --json models --summary` or "
|
|
71
|
+
"`delegate --json capabilities` for reasoning-effort support."
|
|
72
|
+
)
|
|
73
|
+
KIMI_UNSUPPORTED_REASONING_WARNING = "reasoning effort is not supported for kimi."
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class ReasoningProfile:
|
|
78
|
+
transport: str | None
|
|
79
|
+
strategy: str # "model-table" | "static-enum" | "effort-routing" | "unsupported"
|
|
80
|
+
static_efforts: tuple[str, ...] | None = None # static-enum (claude)
|
|
81
|
+
unsupported_warning: str | None = None # unsupported (kimi)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# Single source of truth for per-harness reasoning facts; the renderers below
|
|
85
|
+
# read from this table instead of re-encoding each fact. Row order matters:
|
|
86
|
+
# codex precedes droid so the derived model-table loop emits byte-identical
|
|
87
|
+
# output.
|
|
88
|
+
REASONING_PROFILES: dict[str, ReasoningProfile] = {
|
|
89
|
+
"codex": ReasoningProfile(TRANSPORT_CODEX_CONFIG, "model-table"),
|
|
90
|
+
"droid": ReasoningProfile(TRANSPORT_DROID_FLAG, "model-table"),
|
|
91
|
+
"cursor": ReasoningProfile(TRANSPORT_CURSOR_MODEL_SELECTION, "effort-routing"),
|
|
92
|
+
"claude": ReasoningProfile(
|
|
93
|
+
TRANSPORT_CLAUDE_EFFORT_FLAG,
|
|
94
|
+
"static-enum",
|
|
95
|
+
static_efforts=CLAUDE_NATIVE_EFFORTS,
|
|
96
|
+
),
|
|
97
|
+
"grok": ReasoningProfile(
|
|
98
|
+
TRANSPORT_GROK_EFFORT_FLAG,
|
|
99
|
+
"static-enum",
|
|
100
|
+
static_efforts=GROK_NATIVE_EFFORTS,
|
|
101
|
+
),
|
|
102
|
+
"kimi": ReasoningProfile(
|
|
103
|
+
None,
|
|
104
|
+
"unsupported",
|
|
105
|
+
unsupported_warning=KIMI_UNSUPPORTED_REASONING_WARNING,
|
|
106
|
+
),
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
TRANSPORT_BY_HARNESS = {
|
|
110
|
+
h: p.transport
|
|
111
|
+
for h, p in REASONING_PROFILES.items()
|
|
112
|
+
if p.transport is not None and p.strategy != "static-enum"
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _alias_key_for_default_model(default_model: object) -> str:
|
|
117
|
+
return default_model if isinstance(default_model, str) and default_model else "(default)"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _kimi_unsupported_reasoning_fields() -> JsonObject:
|
|
121
|
+
return {
|
|
122
|
+
"supported": None,
|
|
123
|
+
"source": "none",
|
|
124
|
+
"warning": REASONING_PROFILES["kimi"].unsupported_warning,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _resolved_model_required_detail(harness: str) -> str:
|
|
129
|
+
if harness == "codex":
|
|
130
|
+
return (
|
|
131
|
+
"reasoning effort requires a resolved model — set codex.defaultModel "
|
|
132
|
+
"in config (or pass a model via run-input JSON)"
|
|
133
|
+
)
|
|
134
|
+
if harness == "droid":
|
|
135
|
+
return (
|
|
136
|
+
"reasoning effort requires a resolved model — configure the selected "
|
|
137
|
+
"Droid alias in droid.models"
|
|
138
|
+
)
|
|
139
|
+
return "reasoning effort requires a resolved model"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass(frozen=True)
|
|
143
|
+
class ReasoningCapability:
|
|
144
|
+
harness: str
|
|
145
|
+
model: str
|
|
146
|
+
effort: str
|
|
147
|
+
supported_efforts: tuple[str, ...]
|
|
148
|
+
default_effort: str | None
|
|
149
|
+
transport: str
|
|
150
|
+
source: str
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class ReasoningCapabilityError(Exception):
|
|
154
|
+
def __init__(self, error: str, message: str) -> None:
|
|
155
|
+
super().__init__(message)
|
|
156
|
+
self.error = error
|
|
157
|
+
self.message = message
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class ReasoningPayloadCarrier(Protocol):
|
|
161
|
+
reasoning_effort: str | None
|
|
162
|
+
reasoning_effort_source: str | None
|
|
163
|
+
reasoning_capability_source: str | None
|
|
164
|
+
reasoning_transport: str | None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def is_valid_effort_string(value: object) -> bool:
|
|
168
|
+
# Effort values land in child argv and inside a quoted Codex TOML override
|
|
169
|
+
# (model_reasoning_effort="…"), so quoting hazards are banned alongside
|
|
170
|
+
# whitespace.
|
|
171
|
+
return (
|
|
172
|
+
isinstance(value, str)
|
|
173
|
+
and bool(value)
|
|
174
|
+
and not any(ch.isspace() for ch in value)
|
|
175
|
+
and '"' not in value
|
|
176
|
+
and "\\" not in value
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def normalize_effort(value: object) -> str:
|
|
181
|
+
if not is_valid_effort_string(value):
|
|
182
|
+
raise ReasoningCapabilityError(
|
|
183
|
+
"invalid_reasoning_effort",
|
|
184
|
+
"reasoning effort must be a non-empty string without whitespace, "
|
|
185
|
+
"double quotes, or backslashes.",
|
|
186
|
+
)
|
|
187
|
+
return cast(str, value)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def format_explicit_reasoning_effort_error(
|
|
191
|
+
*,
|
|
192
|
+
harness: str,
|
|
193
|
+
detail: str,
|
|
194
|
+
alias: str | None = None,
|
|
195
|
+
model: str | None = None,
|
|
196
|
+
effort: str | None = None,
|
|
197
|
+
supported: tuple[str, ...] | list[str] | None = None,
|
|
198
|
+
) -> str:
|
|
199
|
+
context_parts: list[str] = []
|
|
200
|
+
if alias:
|
|
201
|
+
context_parts.append(f"alias {alias!r}")
|
|
202
|
+
context_parts.append(f"harness {harness}")
|
|
203
|
+
if model:
|
|
204
|
+
context_parts.append(f"model {model!r}")
|
|
205
|
+
message = detail
|
|
206
|
+
if context_parts:
|
|
207
|
+
message = f"{detail} ({', '.join(context_parts)})"
|
|
208
|
+
parts = [message]
|
|
209
|
+
if effort is not None:
|
|
210
|
+
parts.append(f"Requested effort: {effort!r}.")
|
|
211
|
+
if supported:
|
|
212
|
+
parts.append(f"Supported values: {', '.join(supported)}.")
|
|
213
|
+
parts.append(INSPECT_REASONING_DISCOVERY_HINT)
|
|
214
|
+
return " ".join(parts)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def resolve_native_effort(
|
|
218
|
+
harness: str,
|
|
219
|
+
requested_effort: str | None,
|
|
220
|
+
*,
|
|
221
|
+
alias: str | None = None,
|
|
222
|
+
model: str | None = None,
|
|
223
|
+
) -> str | None:
|
|
224
|
+
"""Validate static native --effort values for harnesses that expose one."""
|
|
225
|
+
if requested_effort is None:
|
|
226
|
+
return None
|
|
227
|
+
profile = REASONING_PROFILES[harness]
|
|
228
|
+
supported = profile.static_efforts or ()
|
|
229
|
+
effort = normalize_effort(requested_effort)
|
|
230
|
+
if effort not in supported:
|
|
231
|
+
raise ReasoningCapabilityError(
|
|
232
|
+
"unsupported_reasoning_effort",
|
|
233
|
+
format_explicit_reasoning_effort_error(
|
|
234
|
+
harness=harness,
|
|
235
|
+
alias=alias,
|
|
236
|
+
model=model,
|
|
237
|
+
effort=effort,
|
|
238
|
+
supported=supported,
|
|
239
|
+
detail="does not support reasoning effort",
|
|
240
|
+
),
|
|
241
|
+
)
|
|
242
|
+
return effort
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def resolve_claude_native_effort(
|
|
246
|
+
requested_effort: str | None,
|
|
247
|
+
*,
|
|
248
|
+
alias: str | None = None,
|
|
249
|
+
model: str | None = None,
|
|
250
|
+
) -> str | None:
|
|
251
|
+
"""Validate Claude Code's native --effort values.
|
|
252
|
+
|
|
253
|
+
Claude Code exposes reasoning effort as a process-level flag, not a
|
|
254
|
+
per-model capability declaration. Keep this separate from
|
|
255
|
+
``resolve_reasoning_capability`` so user/cache model capability tables do
|
|
256
|
+
not imply Claude model catalog support.
|
|
257
|
+
"""
|
|
258
|
+
return resolve_native_effort("claude", requested_effort, alias=alias, model=model)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def resolve_grok_native_effort(
|
|
262
|
+
requested_effort: str | None,
|
|
263
|
+
*,
|
|
264
|
+
alias: str | None = None,
|
|
265
|
+
model: str | None = None,
|
|
266
|
+
) -> str | None:
|
|
267
|
+
"""Validate Grok Build CLI native --effort values."""
|
|
268
|
+
return resolve_native_effort("grok", requested_effort, alias=alias, model=model)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _as_models_map(source: JsonValue) -> dict[str, JsonObject]:
|
|
272
|
+
if not isinstance(source, dict):
|
|
273
|
+
return {}
|
|
274
|
+
models: dict[str, JsonObject] = {}
|
|
275
|
+
for model, declaration in source.items():
|
|
276
|
+
if isinstance(model, str) and model and isinstance(declaration, dict):
|
|
277
|
+
models[model] = declaration
|
|
278
|
+
return models
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _config_model_declarations(config: JsonObject, harness: str) -> dict[str, JsonObject]:
|
|
282
|
+
reasoning = config.get("reasoning")
|
|
283
|
+
if not isinstance(reasoning, dict):
|
|
284
|
+
return {}
|
|
285
|
+
capabilities = reasoning.get("capabilities")
|
|
286
|
+
if not isinstance(capabilities, dict):
|
|
287
|
+
return {}
|
|
288
|
+
return _as_models_map(capabilities.get(harness))
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _cache_model_declarations(cache: JsonObject | None, harness: str) -> dict[str, JsonObject]:
|
|
292
|
+
if not isinstance(cache, dict):
|
|
293
|
+
return {}
|
|
294
|
+
harnesses = cache.get("harnesses")
|
|
295
|
+
if not isinstance(harnesses, dict):
|
|
296
|
+
return {}
|
|
297
|
+
harness_decl = harnesses.get(harness)
|
|
298
|
+
if not isinstance(harness_decl, dict):
|
|
299
|
+
return {}
|
|
300
|
+
return _as_models_map(harness_decl.get("models"))
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _supported_tuple(declaration: ReasoningDeclaration) -> tuple[str, ...] | None:
|
|
304
|
+
raw = declaration.get("supported")
|
|
305
|
+
if isinstance(raw, tuple) and raw and all(isinstance(item, str) and item for item in raw):
|
|
306
|
+
return raw
|
|
307
|
+
if isinstance(raw, list) and raw and all(isinstance(item, str) and item for item in raw):
|
|
308
|
+
return tuple(raw)
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _default_effort(declaration: ReasoningDeclaration, supported: tuple[str, ...]) -> str | None:
|
|
313
|
+
raw = declaration.get("default")
|
|
314
|
+
if raw is None:
|
|
315
|
+
return None
|
|
316
|
+
if isinstance(raw, str) and raw in supported:
|
|
317
|
+
return raw
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _lookup_declaration(
|
|
322
|
+
*,
|
|
323
|
+
harness: str,
|
|
324
|
+
model: str,
|
|
325
|
+
config: JsonObject,
|
|
326
|
+
cache: JsonObject | None,
|
|
327
|
+
) -> tuple[ReasoningDeclaration | None, str]:
|
|
328
|
+
for source, declarations in (
|
|
329
|
+
("config", _config_model_declarations(config, harness)),
|
|
330
|
+
("cache", _cache_model_declarations(cache, harness)),
|
|
331
|
+
("bundled", BUNDLED_REASONING_CAPABILITIES.get(harness, {})),
|
|
332
|
+
):
|
|
333
|
+
declaration = declarations.get(model)
|
|
334
|
+
if declaration is not None:
|
|
335
|
+
return declaration, source
|
|
336
|
+
return None, "none"
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def resolve_reasoning_capability(
|
|
340
|
+
*,
|
|
341
|
+
harness: str,
|
|
342
|
+
model: str | None,
|
|
343
|
+
requested_effort: str | None,
|
|
344
|
+
config: JsonObject,
|
|
345
|
+
cache: JsonObject | None = None,
|
|
346
|
+
alias: str | None = None,
|
|
347
|
+
) -> ReasoningCapability | None:
|
|
348
|
+
if requested_effort is None:
|
|
349
|
+
return None
|
|
350
|
+
effort = normalize_effort(requested_effort)
|
|
351
|
+
if harness not in TRANSPORT_BY_HARNESS:
|
|
352
|
+
raise ReasoningCapabilityError(
|
|
353
|
+
"unsupported_reasoning_effort",
|
|
354
|
+
format_explicit_reasoning_effort_error(
|
|
355
|
+
harness=harness,
|
|
356
|
+
alias=alias,
|
|
357
|
+
model=model,
|
|
358
|
+
detail="reasoning effort is not supported",
|
|
359
|
+
),
|
|
360
|
+
)
|
|
361
|
+
if not model:
|
|
362
|
+
raise ReasoningCapabilityError(
|
|
363
|
+
"unsupported_reasoning_effort",
|
|
364
|
+
format_explicit_reasoning_effort_error(
|
|
365
|
+
harness=harness,
|
|
366
|
+
alias=alias,
|
|
367
|
+
detail=_resolved_model_required_detail(harness),
|
|
368
|
+
),
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
declaration, source = _lookup_declaration(
|
|
372
|
+
harness=harness,
|
|
373
|
+
model=model,
|
|
374
|
+
config=config,
|
|
375
|
+
cache=cache,
|
|
376
|
+
)
|
|
377
|
+
if declaration is None:
|
|
378
|
+
raise ReasoningCapabilityError(
|
|
379
|
+
"unsupported_reasoning_effort",
|
|
380
|
+
format_explicit_reasoning_effort_error(
|
|
381
|
+
harness=harness,
|
|
382
|
+
alias=alias,
|
|
383
|
+
model=model,
|
|
384
|
+
effort=effort,
|
|
385
|
+
detail="has no declared reasoning-effort capability",
|
|
386
|
+
),
|
|
387
|
+
)
|
|
388
|
+
supported = _supported_tuple(declaration)
|
|
389
|
+
if supported is None:
|
|
390
|
+
raise ReasoningCapabilityError(
|
|
391
|
+
"invalid_reasoning_config",
|
|
392
|
+
format_explicit_reasoning_effort_error(
|
|
393
|
+
harness=harness,
|
|
394
|
+
alias=alias,
|
|
395
|
+
model=model,
|
|
396
|
+
effort=effort,
|
|
397
|
+
detail="has malformed reasoning capability data",
|
|
398
|
+
),
|
|
399
|
+
)
|
|
400
|
+
if effort not in supported:
|
|
401
|
+
raise ReasoningCapabilityError(
|
|
402
|
+
"unsupported_reasoning_effort",
|
|
403
|
+
format_explicit_reasoning_effort_error(
|
|
404
|
+
harness=harness,
|
|
405
|
+
alias=alias,
|
|
406
|
+
model=model,
|
|
407
|
+
effort=effort,
|
|
408
|
+
supported=supported,
|
|
409
|
+
detail="does not support reasoning effort",
|
|
410
|
+
),
|
|
411
|
+
)
|
|
412
|
+
return ReasoningCapability(
|
|
413
|
+
harness=harness,
|
|
414
|
+
model=model,
|
|
415
|
+
effort=effort,
|
|
416
|
+
supported_efforts=supported,
|
|
417
|
+
default_effort=_default_effort(declaration, supported),
|
|
418
|
+
transport=TRANSPORT_BY_HARNESS[harness],
|
|
419
|
+
source=source,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def add_reasoning_payload_fields(payload: JsonObject, carrier: ReasoningPayloadCarrier) -> None:
|
|
424
|
+
"""Emit the reasoning JSON fields from any carrier with the shared attribute names.
|
|
425
|
+
|
|
426
|
+
Used for both the CLI Request and the runner RunContext so dry-run payloads,
|
|
427
|
+
manifests, and snapshots cannot drift. The documented schema exposes both
|
|
428
|
+
requestedReasoningEffort and resolvedReasoningEffort; resolution never
|
|
429
|
+
rewrites the value today, so both keys carry the single internal effort.
|
|
430
|
+
"""
|
|
431
|
+
effort = carrier.reasoning_effort
|
|
432
|
+
if effort is not None:
|
|
433
|
+
payload["requestedReasoningEffort"] = effort
|
|
434
|
+
payload["resolvedReasoningEffort"] = effort
|
|
435
|
+
if carrier.reasoning_effort_source is not None:
|
|
436
|
+
payload["reasoningEffortSource"] = carrier.reasoning_effort_source
|
|
437
|
+
if carrier.reasoning_capability_source is not None:
|
|
438
|
+
payload["reasoningCapabilitySource"] = carrier.reasoning_capability_source
|
|
439
|
+
if carrier.reasoning_transport is not None:
|
|
440
|
+
payload["reasoningTransport"] = carrier.reasoning_transport
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def reasoning_capability_cache_path(workspace: str | Path) -> Path:
|
|
444
|
+
return Path(workspace) / ".delegate" / "capabilities" / "reasoning.json"
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def load_reasoning_capability_cache(workspace: str | Path) -> JsonObject | None:
|
|
448
|
+
"""Load the workspace capability cache, treating malformed payloads as absent.
|
|
449
|
+
|
|
450
|
+
The disk read is the trust boundary: a corrupt cache must not block runs
|
|
451
|
+
(config/bundled declarations may still resolve the model) and must not stop
|
|
452
|
+
`capabilities refresh` from overwriting it with fresh data.
|
|
453
|
+
"""
|
|
454
|
+
try:
|
|
455
|
+
loaded = run_registry.read_json_object(reasoning_capability_cache_path(workspace))
|
|
456
|
+
except run_registry.RegistryJsonError:
|
|
457
|
+
return None
|
|
458
|
+
if loaded is None:
|
|
459
|
+
return None
|
|
460
|
+
try:
|
|
461
|
+
validate_cache_payload(loaded)
|
|
462
|
+
except ReasoningCapabilityError:
|
|
463
|
+
return None
|
|
464
|
+
return loaded
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _copy_payload_model(declaration: ReasoningDeclaration, *, source: str) -> JsonObject | None:
|
|
468
|
+
supported = _supported_tuple(declaration)
|
|
469
|
+
if supported is None:
|
|
470
|
+
return None
|
|
471
|
+
payload: JsonObject = {"supported": list(supported), "source": source}
|
|
472
|
+
default = _default_effort(declaration, supported)
|
|
473
|
+
if default is not None:
|
|
474
|
+
payload["default"] = default
|
|
475
|
+
return payload
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _overlay_payload_models(
|
|
479
|
+
payload: JsonObject, declarations: Mapping[str, ReasoningDeclaration], source: str
|
|
480
|
+
) -> None:
|
|
481
|
+
for model, declaration in sorted(declarations.items()):
|
|
482
|
+
model_payload = _copy_payload_model(declaration, source=source)
|
|
483
|
+
if model_payload is not None:
|
|
484
|
+
payload[model] = model_payload
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _cursor_reasoning_models_payload(mappings: JsonValue) -> JsonObject:
|
|
488
|
+
models: JsonObject = {}
|
|
489
|
+
if not isinstance(mappings, dict):
|
|
490
|
+
return models
|
|
491
|
+
for effort, model in sorted(mappings.items()):
|
|
492
|
+
if not (isinstance(effort, str) and effort and isinstance(model, str) and model):
|
|
493
|
+
continue
|
|
494
|
+
model_payload = models.setdefault(model, {"supported": [], "source": "config"})
|
|
495
|
+
supported = model_payload["supported"]
|
|
496
|
+
if isinstance(supported, list):
|
|
497
|
+
supported.append(effort)
|
|
498
|
+
return models
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _model_reasoning_summary(
|
|
502
|
+
*,
|
|
503
|
+
harness: str,
|
|
504
|
+
alias: str,
|
|
505
|
+
model: str | None,
|
|
506
|
+
config: JsonObject,
|
|
507
|
+
cache: JsonObject | None,
|
|
508
|
+
config_default: str | None = None,
|
|
509
|
+
) -> JsonObject:
|
|
510
|
+
payload: JsonObject = {"alias": alias}
|
|
511
|
+
if model:
|
|
512
|
+
payload["model"] = model
|
|
513
|
+
else:
|
|
514
|
+
payload["supported"] = None
|
|
515
|
+
payload["source"] = "none"
|
|
516
|
+
payload["warning"] = f"{harness} requires a resolved model for reasoning-effort support."
|
|
517
|
+
return payload
|
|
518
|
+
|
|
519
|
+
declaration, source = _lookup_declaration(
|
|
520
|
+
harness=harness,
|
|
521
|
+
model=model,
|
|
522
|
+
config=config,
|
|
523
|
+
cache=cache,
|
|
524
|
+
)
|
|
525
|
+
if declaration is None:
|
|
526
|
+
payload["supported"] = None
|
|
527
|
+
payload["source"] = "none"
|
|
528
|
+
payload["warning"] = (
|
|
529
|
+
f"no declared reasoning-effort capability for {harness} model {model!r}."
|
|
530
|
+
)
|
|
531
|
+
return payload
|
|
532
|
+
|
|
533
|
+
supported = _supported_tuple(declaration)
|
|
534
|
+
if supported is None:
|
|
535
|
+
payload["supported"] = None
|
|
536
|
+
payload["source"] = source
|
|
537
|
+
payload["warning"] = f"malformed reasoning capability data for {harness} model {model!r}."
|
|
538
|
+
return payload
|
|
539
|
+
|
|
540
|
+
payload["supported"] = list(supported)
|
|
541
|
+
payload["source"] = source
|
|
542
|
+
default = _default_effort(declaration, supported)
|
|
543
|
+
if default is not None:
|
|
544
|
+
payload["default"] = default
|
|
545
|
+
if config_default is not None:
|
|
546
|
+
payload["configDefault"] = config_default
|
|
547
|
+
return payload
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _cursor_alias_reasoning_summary(cursor: JsonObject) -> JsonObject:
|
|
551
|
+
default_model = cursor.get("defaultModel")
|
|
552
|
+
alias = _alias_key_for_default_model(default_model)
|
|
553
|
+
payload: JsonObject = {"alias": alias}
|
|
554
|
+
if isinstance(default_model, str) and default_model:
|
|
555
|
+
payload["defaultModel"] = default_model
|
|
556
|
+
mappings = cursor.get("reasoningEffortModels")
|
|
557
|
+
routing: list[JsonObject] = []
|
|
558
|
+
if isinstance(mappings, dict):
|
|
559
|
+
routing = [
|
|
560
|
+
{"effort": effort, "model": model}
|
|
561
|
+
for effort, model in sorted(mappings.items())
|
|
562
|
+
if isinstance(effort, str) and effort and isinstance(model, str) and model
|
|
563
|
+
]
|
|
564
|
+
if routing:
|
|
565
|
+
payload["effortModelRouting"] = routing
|
|
566
|
+
payload["source"] = "config"
|
|
567
|
+
else:
|
|
568
|
+
payload["source"] = "none"
|
|
569
|
+
payload["warning"] = (
|
|
570
|
+
"cursor.reasoningEffortModels is empty; configure effort-to-model mappings."
|
|
571
|
+
)
|
|
572
|
+
default_effort = cursor.get("defaultReasoningEffort")
|
|
573
|
+
if isinstance(default_effort, str):
|
|
574
|
+
payload["configDefault"] = default_effort
|
|
575
|
+
return payload
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _claude_alias_reasoning_summary(claude: JsonObject) -> JsonObject:
|
|
579
|
+
default_model = claude.get("defaultModel")
|
|
580
|
+
alias = _alias_key_for_default_model(default_model)
|
|
581
|
+
payload: JsonObject = {
|
|
582
|
+
"alias": alias,
|
|
583
|
+
"supported": list(REASONING_PROFILES["claude"].static_efforts),
|
|
584
|
+
"source": "static",
|
|
585
|
+
"transport": REASONING_PROFILES["claude"].transport,
|
|
586
|
+
}
|
|
587
|
+
if isinstance(default_model, str) and default_model:
|
|
588
|
+
payload["model"] = default_model
|
|
589
|
+
default_effort = claude.get("defaultReasoningEffort")
|
|
590
|
+
if isinstance(default_effort, str):
|
|
591
|
+
payload["configDefault"] = default_effort
|
|
592
|
+
return payload
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _grok_alias_reasoning_summary(grok: JsonObject) -> JsonObject:
|
|
596
|
+
default_model = grok.get("defaultModel")
|
|
597
|
+
alias = _alias_key_for_default_model(default_model)
|
|
598
|
+
payload: JsonObject = {
|
|
599
|
+
"alias": alias,
|
|
600
|
+
"supported": list(REASONING_PROFILES["grok"].static_efforts),
|
|
601
|
+
"source": "static",
|
|
602
|
+
"transport": REASONING_PROFILES["grok"].transport,
|
|
603
|
+
}
|
|
604
|
+
if isinstance(default_model, str) and default_model:
|
|
605
|
+
payload["model"] = default_model
|
|
606
|
+
default_effort = grok.get("defaultReasoningEffort")
|
|
607
|
+
if isinstance(default_effort, str):
|
|
608
|
+
payload["configDefault"] = default_effort
|
|
609
|
+
return payload
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def _kimi_alias_reasoning_summary(kimi: JsonObject) -> JsonObject:
|
|
613
|
+
default_model = kimi.get("defaultModel")
|
|
614
|
+
alias = _alias_key_for_default_model(default_model)
|
|
615
|
+
payload: JsonObject = {"alias": alias, **_kimi_unsupported_reasoning_fields()}
|
|
616
|
+
if isinstance(default_model, str) and default_model:
|
|
617
|
+
payload["model"] = default_model
|
|
618
|
+
return payload
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def build_alias_reasoning_summaries(
|
|
622
|
+
config: JsonObject,
|
|
623
|
+
cache: JsonObject | None,
|
|
624
|
+
) -> JsonObject:
|
|
625
|
+
summaries: JsonObject = {}
|
|
626
|
+
|
|
627
|
+
droid = config.get("droid")
|
|
628
|
+
droid_aliases: JsonObject = {}
|
|
629
|
+
if isinstance(droid, dict):
|
|
630
|
+
models = droid.get("models")
|
|
631
|
+
config_default = droid.get("defaultReasoningEffort")
|
|
632
|
+
default_effort = config_default if isinstance(config_default, str) else None
|
|
633
|
+
if isinstance(models, dict):
|
|
634
|
+
for alias, model_id in sorted(models.items()):
|
|
635
|
+
if not (
|
|
636
|
+
isinstance(alias, str) and alias and isinstance(model_id, str) and model_id
|
|
637
|
+
):
|
|
638
|
+
continue
|
|
639
|
+
droid_aliases[alias] = _model_reasoning_summary(
|
|
640
|
+
harness="droid",
|
|
641
|
+
alias=alias,
|
|
642
|
+
model=model_id,
|
|
643
|
+
config=config,
|
|
644
|
+
cache=cache,
|
|
645
|
+
config_default=default_effort,
|
|
646
|
+
)
|
|
647
|
+
summaries["droid"] = droid_aliases
|
|
648
|
+
|
|
649
|
+
codex = config.get("codex")
|
|
650
|
+
codex_aliases: JsonObject = {}
|
|
651
|
+
if isinstance(codex, dict):
|
|
652
|
+
default_model = codex.get("defaultModel")
|
|
653
|
+
config_default = codex.get("defaultReasoningEffort")
|
|
654
|
+
default_effort = config_default if isinstance(config_default, str) else None
|
|
655
|
+
if isinstance(default_model, str) and default_model:
|
|
656
|
+
codex_aliases[default_model] = _model_reasoning_summary(
|
|
657
|
+
harness="codex",
|
|
658
|
+
alias=default_model,
|
|
659
|
+
model=default_model,
|
|
660
|
+
config=config,
|
|
661
|
+
cache=cache,
|
|
662
|
+
config_default=default_effort,
|
|
663
|
+
)
|
|
664
|
+
else:
|
|
665
|
+
codex_aliases["(unconfigured)"] = {
|
|
666
|
+
"alias": "(unconfigured)",
|
|
667
|
+
"supported": None,
|
|
668
|
+
"source": "none",
|
|
669
|
+
"warning": "codex.defaultModel is not set; reasoning effort requires a resolved model.",
|
|
670
|
+
}
|
|
671
|
+
summaries["codex"] = codex_aliases
|
|
672
|
+
|
|
673
|
+
cursor = config.get("cursor")
|
|
674
|
+
if isinstance(cursor, dict):
|
|
675
|
+
default_model = cursor.get("defaultModel")
|
|
676
|
+
alias_key = _alias_key_for_default_model(default_model)
|
|
677
|
+
summaries["cursor"] = {alias_key: _cursor_alias_reasoning_summary(cursor)}
|
|
678
|
+
else:
|
|
679
|
+
summaries["cursor"] = {}
|
|
680
|
+
|
|
681
|
+
claude = config.get("claude")
|
|
682
|
+
if isinstance(claude, dict):
|
|
683
|
+
default_model = claude.get("defaultModel")
|
|
684
|
+
alias_key = _alias_key_for_default_model(default_model)
|
|
685
|
+
summaries["claude"] = {alias_key: _claude_alias_reasoning_summary(claude)}
|
|
686
|
+
else:
|
|
687
|
+
summaries["claude"] = {}
|
|
688
|
+
|
|
689
|
+
grok = config.get("grok")
|
|
690
|
+
if isinstance(grok, dict):
|
|
691
|
+
default_model = grok.get("defaultModel")
|
|
692
|
+
alias_key = _alias_key_for_default_model(default_model)
|
|
693
|
+
summaries["grok"] = {alias_key: _grok_alias_reasoning_summary(grok)}
|
|
694
|
+
else:
|
|
695
|
+
summaries["grok"] = {}
|
|
696
|
+
|
|
697
|
+
kimi = config.get("kimi")
|
|
698
|
+
if isinstance(kimi, dict):
|
|
699
|
+
default_model = kimi.get("defaultModel")
|
|
700
|
+
alias_key = _alias_key_for_default_model(default_model)
|
|
701
|
+
summaries["kimi"] = {alias_key: _kimi_alias_reasoning_summary(kimi)}
|
|
702
|
+
else:
|
|
703
|
+
summaries["kimi"] = {}
|
|
704
|
+
|
|
705
|
+
return summaries
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
def build_reasoning_capabilities_payload(
|
|
709
|
+
config: JsonObject,
|
|
710
|
+
cache: JsonObject | None,
|
|
711
|
+
) -> JsonObject:
|
|
712
|
+
harnesses: JsonObject = {}
|
|
713
|
+
for harness in [h for h, p in REASONING_PROFILES.items() if p.strategy == "model-table"]:
|
|
714
|
+
models_payload: JsonObject = {}
|
|
715
|
+
_overlay_payload_models(
|
|
716
|
+
models_payload,
|
|
717
|
+
BUNDLED_REASONING_CAPABILITIES.get(harness, {}),
|
|
718
|
+
"bundled",
|
|
719
|
+
)
|
|
720
|
+
_overlay_payload_models(models_payload, _cache_model_declarations(cache, harness), "cache")
|
|
721
|
+
_overlay_payload_models(
|
|
722
|
+
models_payload,
|
|
723
|
+
_config_model_declarations(config, harness),
|
|
724
|
+
"config",
|
|
725
|
+
)
|
|
726
|
+
harnesses[harness] = {
|
|
727
|
+
"transport": TRANSPORT_BY_HARNESS[harness],
|
|
728
|
+
"models": models_payload,
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
cursor = config.get("cursor")
|
|
732
|
+
mappings = cursor.get("reasoningEffortModels") if isinstance(cursor, dict) else None
|
|
733
|
+
harnesses["cursor"] = {
|
|
734
|
+
"transport": TRANSPORT_BY_HARNESS["cursor"],
|
|
735
|
+
"models": _cursor_reasoning_models_payload(mappings),
|
|
736
|
+
}
|
|
737
|
+
harnesses["claude"] = {
|
|
738
|
+
"transport": REASONING_PROFILES["claude"].transport,
|
|
739
|
+
"source": "static",
|
|
740
|
+
"supported": list(REASONING_PROFILES["claude"].static_efforts),
|
|
741
|
+
"models": {},
|
|
742
|
+
}
|
|
743
|
+
harnesses["grok"] = {
|
|
744
|
+
"transport": REASONING_PROFILES["grok"].transport,
|
|
745
|
+
"source": "static",
|
|
746
|
+
"supported": list(REASONING_PROFILES["grok"].static_efforts),
|
|
747
|
+
"models": {},
|
|
748
|
+
}
|
|
749
|
+
harnesses["kimi"] = {
|
|
750
|
+
"transport": REASONING_PROFILES["kimi"].transport,
|
|
751
|
+
**_kimi_unsupported_reasoning_fields(),
|
|
752
|
+
"models": {},
|
|
753
|
+
}
|
|
754
|
+
return {"harnesses": harnesses, "aliases": build_alias_reasoning_summaries(config, cache)}
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def validate_cache_payload(cache: JsonObject) -> None:
|
|
758
|
+
harnesses = cache.get("harnesses")
|
|
759
|
+
if not isinstance(harnesses, dict):
|
|
760
|
+
raise ReasoningCapabilityError(
|
|
761
|
+
"invalid_reasoning_config",
|
|
762
|
+
"reasoning cache must contain a harnesses object.",
|
|
763
|
+
)
|
|
764
|
+
for harness, harness_decl in harnesses.items():
|
|
765
|
+
if harness not in TRANSPORT_BY_HARNESS or not isinstance(harness_decl, dict):
|
|
766
|
+
raise ReasoningCapabilityError(
|
|
767
|
+
"invalid_reasoning_config",
|
|
768
|
+
"reasoning cache harness entries must be objects for known harnesses.",
|
|
769
|
+
)
|
|
770
|
+
models = harness_decl.get("models")
|
|
771
|
+
if not isinstance(models, dict):
|
|
772
|
+
raise ReasoningCapabilityError(
|
|
773
|
+
"invalid_reasoning_config",
|
|
774
|
+
f"reasoning cache {harness}.models must be an object.",
|
|
775
|
+
)
|
|
776
|
+
for model, declaration in models.items():
|
|
777
|
+
if not isinstance(model, str) or not model or not isinstance(declaration, dict):
|
|
778
|
+
raise ReasoningCapabilityError(
|
|
779
|
+
"invalid_reasoning_config",
|
|
780
|
+
f"reasoning cache {harness}.models has malformed model declarations.",
|
|
781
|
+
)
|
|
782
|
+
supported = _supported_tuple(declaration)
|
|
783
|
+
if supported is None:
|
|
784
|
+
raise ReasoningCapabilityError(
|
|
785
|
+
"invalid_reasoning_config",
|
|
786
|
+
f"reasoning cache {harness}.{model}.supported must be non-empty strings.",
|
|
787
|
+
)
|
|
788
|
+
if (
|
|
789
|
+
declaration.get("default") is not None
|
|
790
|
+
and _default_effort(declaration, supported) is None
|
|
791
|
+
):
|
|
792
|
+
raise ReasoningCapabilityError(
|
|
793
|
+
"invalid_reasoning_config",
|
|
794
|
+
f"reasoning cache {harness}.{model}.default must be in supported.",
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def parse_codex_models_payload(raw: JsonObject) -> JsonObject:
|
|
799
|
+
models = raw.get("models")
|
|
800
|
+
if not isinstance(models, list):
|
|
801
|
+
raise ReasoningCapabilityError(
|
|
802
|
+
"capability_refresh_failed",
|
|
803
|
+
"codex model payload must contain a models array.",
|
|
804
|
+
)
|
|
805
|
+
target: JsonObject = {}
|
|
806
|
+
parsed: JsonObject = {"schema": 1, "harnesses": {"codex": {"models": target}}}
|
|
807
|
+
for item in models:
|
|
808
|
+
if not isinstance(item, dict):
|
|
809
|
+
raise ReasoningCapabilityError(
|
|
810
|
+
"capability_refresh_failed", "codex model entries must be objects."
|
|
811
|
+
)
|
|
812
|
+
slug = item.get("slug")
|
|
813
|
+
raw_supported = item.get("supported_reasoning_levels")
|
|
814
|
+
if not isinstance(slug, str) or not slug:
|
|
815
|
+
raise ReasoningCapabilityError(
|
|
816
|
+
"capability_refresh_failed", "codex model entries require slug."
|
|
817
|
+
)
|
|
818
|
+
supported: list[str] = []
|
|
819
|
+
if isinstance(raw_supported, list):
|
|
820
|
+
for level in raw_supported:
|
|
821
|
+
if isinstance(level, str) and level:
|
|
822
|
+
supported.append(level)
|
|
823
|
+
elif (
|
|
824
|
+
isinstance(level, dict)
|
|
825
|
+
and isinstance(level.get("effort"), str)
|
|
826
|
+
and level["effort"]
|
|
827
|
+
):
|
|
828
|
+
supported.append(level["effort"])
|
|
829
|
+
if not supported:
|
|
830
|
+
raise ReasoningCapabilityError(
|
|
831
|
+
"capability_refresh_failed",
|
|
832
|
+
f"codex model {slug!r} has no supported reasoning levels.",
|
|
833
|
+
)
|
|
834
|
+
default = item.get("default_reasoning_level")
|
|
835
|
+
declaration: JsonObject = {"supported": supported}
|
|
836
|
+
if isinstance(default, str) and default:
|
|
837
|
+
declaration["default"] = default
|
|
838
|
+
target[slug] = declaration
|
|
839
|
+
validate_cache_payload(parsed)
|
|
840
|
+
return parsed
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def refresh_reasoning_capabilities(
|
|
844
|
+
*,
|
|
845
|
+
cwd: str,
|
|
846
|
+
codex_binary: str = "codex",
|
|
847
|
+
env: dict[str, str] | None = None,
|
|
848
|
+
) -> JsonObject:
|
|
849
|
+
try:
|
|
850
|
+
completed = subprocess.run( # nosec B603 - configured Codex binary is executed with shell=False and a timeout.
|
|
851
|
+
[codex_binary, "debug", "models"],
|
|
852
|
+
cwd=cwd,
|
|
853
|
+
env=env,
|
|
854
|
+
text=True,
|
|
855
|
+
capture_output=True,
|
|
856
|
+
check=False,
|
|
857
|
+
timeout=30,
|
|
858
|
+
)
|
|
859
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
860
|
+
raise ReasoningCapabilityError(
|
|
861
|
+
"capability_refresh_failed",
|
|
862
|
+
f"could not run {codex_binary} debug models: {exc}",
|
|
863
|
+
) from exc
|
|
864
|
+
if completed.returncode != 0:
|
|
865
|
+
stderr = completed.stderr.strip()
|
|
866
|
+
raise ReasoningCapabilityError(
|
|
867
|
+
"capability_refresh_failed",
|
|
868
|
+
f"{codex_binary} debug models failed: {stderr or completed.returncode}",
|
|
869
|
+
)
|
|
870
|
+
try:
|
|
871
|
+
raw: JsonValue = json.loads(completed.stdout)
|
|
872
|
+
except json.JSONDecodeError as exc:
|
|
873
|
+
raise ReasoningCapabilityError(
|
|
874
|
+
"capability_refresh_failed",
|
|
875
|
+
f"{codex_binary} debug models did not return JSON: {exc}",
|
|
876
|
+
) from exc
|
|
877
|
+
if not isinstance(raw, dict):
|
|
878
|
+
raise ReasoningCapabilityError(
|
|
879
|
+
"capability_refresh_failed",
|
|
880
|
+
f"{codex_binary} debug models returned a non-object payload.",
|
|
881
|
+
)
|
|
882
|
+
return parse_codex_models_payload(raw)
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def merge_reasoning_capability_cache(
|
|
886
|
+
existing: JsonObject | None,
|
|
887
|
+
refreshed: JsonObject,
|
|
888
|
+
) -> JsonObject:
|
|
889
|
+
"""Overlay refreshed harness declarations on the existing cache.
|
|
890
|
+
|
|
891
|
+
Inputs are already validated at their boundaries (parse_codex_models_payload
|
|
892
|
+
for refreshed data, load_reasoning_capability_cache for the existing file),
|
|
893
|
+
and write_reasoning_capability_cache validates the merged result.
|
|
894
|
+
"""
|
|
895
|
+
harnesses: JsonObject = {}
|
|
896
|
+
if existing is not None:
|
|
897
|
+
existing_harnesses = existing.get("harnesses")
|
|
898
|
+
if isinstance(existing_harnesses, dict):
|
|
899
|
+
harnesses.update(existing_harnesses)
|
|
900
|
+
refreshed_harnesses = refreshed["harnesses"]
|
|
901
|
+
if not isinstance(refreshed_harnesses, dict):
|
|
902
|
+
raise ReasoningCapabilityError(
|
|
903
|
+
"capability_refresh_failed",
|
|
904
|
+
"refreshed reasoning cache must contain a harnesses object.",
|
|
905
|
+
)
|
|
906
|
+
harnesses.update(refreshed_harnesses)
|
|
907
|
+
return {"schema": 1, "harnesses": harnesses}
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def write_reasoning_capability_cache(workspace: str | Path, cache: JsonObject) -> Path:
|
|
911
|
+
validate_cache_payload(cache)
|
|
912
|
+
path = reasoning_capability_cache_path(workspace)
|
|
913
|
+
# write_json_atomic applies the registry's owner-only file/dir modes, keeping
|
|
914
|
+
# this .delegate artifact as private as run manifests and logs.
|
|
915
|
+
run_registry.write_json_atomic(path, cache)
|
|
916
|
+
return path
|