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.
Files changed (54) hide show
  1. delegate_agent/__init__.py +5 -0
  2. delegate_agent/archived_logs.py +44 -0
  3. delegate_agent/argv_builders.py +423 -0
  4. delegate_agent/argv_utils.py +36 -0
  5. delegate_agent/capability_commands.py +99 -0
  6. delegate_agent/cli.py +987 -0
  7. delegate_agent/cli_parser.py +1627 -0
  8. delegate_agent/command_errors.py +29 -0
  9. delegate_agent/command_help.py +1264 -0
  10. delegate_agent/config.py +1117 -0
  11. delegate_agent/config_commands.py +143 -0
  12. delegate_agent/constants.py +50 -0
  13. delegate_agent/describe_payload.py +1018 -0
  14. delegate_agent/errors.py +32 -0
  15. delegate_agent/git_utils.py +180 -0
  16. delegate_agent/harness_events.py +719 -0
  17. delegate_agent/inspection_commands.py +104 -0
  18. delegate_agent/isolation.py +316 -0
  19. delegate_agent/json_types.py +18 -0
  20. delegate_agent/log_output.py +78 -0
  21. delegate_agent/private_io.py +109 -0
  22. delegate_agent/profile_commands.py +42 -0
  23. delegate_agent/profile_guard.py +116 -0
  24. delegate_agent/profiles.py +389 -0
  25. delegate_agent/prompt_instructions.py +39 -0
  26. delegate_agent/prompt_transport.py +12 -0
  27. delegate_agent/reasoning.py +916 -0
  28. delegate_agent/redaction.py +193 -0
  29. delegate_agent/rendering.py +573 -0
  30. delegate_agent/request_build.py +1681 -0
  31. delegate_agent/request_models.py +191 -0
  32. delegate_agent/retention.py +321 -0
  33. delegate_agent/run_metadata.py +78 -0
  34. delegate_agent/run_output_commands.py +645 -0
  35. delegate_agent/run_registry.py +747 -0
  36. delegate_agent/run_status.py +300 -0
  37. delegate_agent/runner.py +1830 -0
  38. delegate_agent/safe_workspace.py +821 -0
  39. delegate_agent/snapshot_view.py +229 -0
  40. delegate_agent/wait_cancel_commands.py +559 -0
  41. delegate_agent/worktree_commands.py +218 -0
  42. delegate_agent/worktree_execution.py +654 -0
  43. delegate_agent/worktree_gc.py +529 -0
  44. delegate_agent/worktree_mgmt.py +782 -0
  45. delegate_agent/worktree_records.py +232 -0
  46. delegate_agent/worktree_remove.py +547 -0
  47. delegate_agent/worktree_summary.py +242 -0
  48. delegate_agent/wsl.py +65 -0
  49. delegate_agent_cli-0.11.0.dist-info/METADATA +325 -0
  50. delegate_agent_cli-0.11.0.dist-info/RECORD +54 -0
  51. delegate_agent_cli-0.11.0.dist-info/WHEEL +5 -0
  52. delegate_agent_cli-0.11.0.dist-info/entry_points.txt +2 -0
  53. delegate_agent_cli-0.11.0.dist-info/licenses/LICENSE +21 -0
  54. delegate_agent_cli-0.11.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1681 @@
1
+ """Request construction.
2
+
3
+ Turns a parsed command plus resolved config into a launch-ready ``Request``:
4
+ workspace/prompt resolution, per-engine request-parts assembly (dispatched via
5
+ ``ENGINE_REQUEST_PARTS_BUILDERS``), reasoning-effort resolution, and the
6
+ JSON/stdin request entry points. The per-engine parts stay explicit because the
7
+ engines differ materially; the argv strings themselves are built in
8
+ ``argv_builders``.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import io
14
+ import json
15
+ import os
16
+ import select
17
+ import shutil
18
+ import subprocess # nosec B404 - Delegate inspects git workspaces with shell=False.
19
+ import tempfile
20
+ from collections.abc import Callable
21
+ from dataclasses import replace
22
+ from pathlib import Path
23
+ from typing import TextIO
24
+
25
+ from delegate_agent import config as delegate_config
26
+ from delegate_agent import profiles, reasoning, safe_workspace, wsl
27
+ from delegate_agent import runner as delegate_runner
28
+ from delegate_agent.argv_builders import (
29
+ SAFE_REVIEW_PREFIX_BY_ENGINE,
30
+ _claude_harness_bypass_enabled,
31
+ _grok_harness_bypass_enabled,
32
+ build_claude_argv,
33
+ build_codex_argv,
34
+ build_cursor_argv,
35
+ build_droid_argv,
36
+ build_grok_argv,
37
+ build_kimi_argv,
38
+ prefix_droid_safe_prompt,
39
+ redacted_prompt_argv,
40
+ )
41
+ from delegate_agent.constants import (
42
+ ENGINES_PROSE,
43
+ KNOWN_ENGINES,
44
+ MODE_CALL,
45
+ MODE_SAFE,
46
+ MODE_WORK,
47
+ MODELESS_NONCURSOR_ENGINES,
48
+ SAFE_REVIEW_PREFIX_INJECTED_HERE_ENGINES,
49
+ validate_mode,
50
+ )
51
+ from delegate_agent.errors import DelegateError
52
+ from delegate_agent.git_utils import GIT_QUICK_TIMEOUT_SECONDS, capture_git_metadata
53
+ from delegate_agent.git_utils import run_git as _run_git
54
+ from delegate_agent.isolation import IsolationContext, build_isolation_context
55
+ from delegate_agent.json_types import JsonObject, JsonValue
56
+ from delegate_agent.prompt_transport import (
57
+ DROID_PROMPT_FILE_ARG_PLACEHOLDER,
58
+ DROID_PROMPT_FILE_DISPLAY,
59
+ KIMI_PROMPT_REDACTION,
60
+ PROMPT_FILE_ARG_PLACEHOLDER,
61
+ PROMPT_FILE_DISPLAY,
62
+ PROMPT_TRANSPORT_ARGV,
63
+ PROMPT_TRANSPORT_FILE,
64
+ PROMPT_TRANSPORT_STDIN,
65
+ )
66
+ from delegate_agent.request_models import (
67
+ EngineBuildInput,
68
+ EngineRequestParts,
69
+ ParsedCommand,
70
+ Request,
71
+ ResolvedWorkspace,
72
+ )
73
+
74
+ RUN_INPUT_KEYS = {
75
+ "engine",
76
+ "mode",
77
+ "model",
78
+ "cwd",
79
+ "prompt",
80
+ "isolation",
81
+ "reasoningEffort",
82
+ "outputSchema",
83
+ "progress",
84
+ "forbidCommit",
85
+ "readOnly",
86
+ "includeDirty",
87
+ }
88
+
89
+ OUTPUT_SCHEMA_COMPLETION_REPORT_WARNING = (
90
+ "--output-schema enforces a JSON-only final message; completion-report instruction "
91
+ "suppressed for this run."
92
+ )
93
+ CALL_TEMP_CWD_PLACEHOLDER = "<delegate-call-temp-cwd>"
94
+ CODEX_HARNESS_DEFAULT_REASONING_EFFORTS = ("low", "medium", "high", "xhigh")
95
+
96
+ # Read-only call is the stateless "judge/completion" contract: text in, text out,
97
+ # no tree. These harnesses default to a coding-agent framing ("inspect the
98
+ # workspace") that derails a judge prompt on an empty cwd, so neutralize that
99
+ # framing. The no-mutation clause is load-bearing for cursor/droid/kimi, whose
100
+ # read-only call has no CLI sandbox — the prompt is the only write boundary there
101
+ # (codex/claude/grok also get a real read-only sandbox flag). Work-level call is
102
+ # left raw — it may legitimately act in the cwd.
103
+ CALL_READONLY_PREAMBLE = (
104
+ "You are being called to respond to the following prompt directly. There is "
105
+ "no repository, working tree, or codebase to inspect, open, or review, and "
106
+ "you must not create, edit, delete, or execute anything — produce your answer "
107
+ "from the prompt text alone.\n\n"
108
+ )
109
+
110
+
111
+ def _call_effective_prompt(prompt: str, *, read_only: bool) -> str:
112
+ if read_only and not prompt.startswith(CALL_READONLY_PREAMBLE):
113
+ return f"{CALL_READONLY_PREAMBLE}{prompt}"
114
+ return prompt
115
+
116
+
117
+ def _policy_mode(build: EngineBuildInput) -> str:
118
+ """Resolve which policy tier a call inherits: default call is work-level, a
119
+ read-only call is safe-level. Bypass flags stay bound to real work mode via
120
+ the argv builders' own ``mode == MODE_WORK`` gates, so borrowing the work
121
+ policy tier here only carries webSearch/networkAccess, never a bypass."""
122
+ if build.mode == MODE_CALL:
123
+ return MODE_SAFE if build.call_read_only else MODE_WORK
124
+ return build.mode
125
+
126
+
127
+ def _reject_windows_path(value: str, field: str) -> None:
128
+ if wsl.should_reject_windows_path(value):
129
+ raise DelegateError("windows_path", wsl.windows_path_message(field, value))
130
+
131
+
132
+ def load_config(
133
+ path: Path | None = None,
134
+ *,
135
+ workspace: Path | None = None,
136
+ cli_overrides: JsonObject | None = None,
137
+ ) -> tuple[JsonObject, str]:
138
+ try:
139
+ return delegate_config.load_config(path, workspace=workspace, cli_overrides=cli_overrides)
140
+ except delegate_config.ConfigError as exc:
141
+ raise DelegateError(exc.error, exc.message) from exc
142
+
143
+
144
+ def validate_config(config: JsonObject) -> None:
145
+ try:
146
+ delegate_config.validate_config(config)
147
+ except delegate_config.ConfigError as exc:
148
+ raise DelegateError(exc.error, exc.message) from exc
149
+
150
+
151
+ ProgressIntent = str | None
152
+
153
+
154
+ def resolve_effective_progress(intent: ProgressIntent, config: JsonObject) -> bool:
155
+ if intent == "off":
156
+ return False
157
+ if intent == "on":
158
+ return True
159
+ progress_section = config.get("progress")
160
+ if isinstance(progress_section, dict):
161
+ enabled = progress_section.get("enabled", False)
162
+ if isinstance(enabled, bool):
163
+ return enabled
164
+ return False
165
+
166
+
167
+ def resolve_progress_timing(config: JsonObject) -> tuple[float, float]:
168
+ default_initial = delegate_config.default_progress_initial_delay_sec()
169
+ default_interval = delegate_config.default_progress_interval_sec()
170
+ progress_section = config.get("progress")
171
+ if not isinstance(progress_section, dict):
172
+ return (default_initial, default_interval)
173
+ initial = progress_section.get("initialDelaySec", default_initial)
174
+ interval = progress_section.get("intervalSec", default_interval)
175
+ return float(initial), float(interval)
176
+
177
+
178
+ def resolve_workspace(global_cwd: str | None, json_cwd: str | None = None) -> ResolvedWorkspace:
179
+ if global_cwd and json_cwd:
180
+ global_workspace = workspace_for(global_cwd)
181
+ json_workspace = workspace_for(json_cwd)
182
+ if Path(global_workspace.path).resolve() != Path(json_workspace.path).resolve():
183
+ raise DelegateError(
184
+ "ambiguous_cwd", "CLI --cwd and JSON cwd resolve to different workspaces."
185
+ )
186
+ return global_workspace
187
+ if json_cwd:
188
+ return workspace_for(json_cwd)
189
+ if global_cwd:
190
+ return workspace_for(global_cwd)
191
+ return workspace_for(os.getcwd())
192
+
193
+
194
+ def workspace_for(path_text: str) -> ResolvedWorkspace:
195
+ _reject_windows_path(path_text, "cwd")
196
+ path = Path(path_text).expanduser().resolve()
197
+ if not path.exists() or not path.is_dir():
198
+ raise DelegateError("invalid_cwd", f"cwd does not exist or is not a directory: {path}")
199
+ git_root = git_root_for(path)
200
+ if git_root is not None:
201
+ return ResolvedWorkspace(git_root, "git")
202
+ return ResolvedWorkspace(str(path), "directory")
203
+
204
+
205
+ def git_root_for(path: Path) -> str | None:
206
+ message = wsl.windows_git_message(shutil.which("git"))
207
+ if message is not None:
208
+ raise DelegateError("windows_git_in_wsl", message)
209
+ try:
210
+ result = _run_git(
211
+ str(path),
212
+ ["rev-parse", "--show-toplevel"],
213
+ timeout_seconds=GIT_QUICK_TIMEOUT_SECONDS,
214
+ )
215
+ except (FileNotFoundError, subprocess.SubprocessError):
216
+ return None
217
+ if result.returncode != 0:
218
+ return None
219
+ return str(Path(result.stdout.strip()).resolve())
220
+
221
+
222
+ def resolve_prompt(
223
+ prompt_parts: list[str] | None,
224
+ prompt_file: str | None,
225
+ stdin: TextIO,
226
+ ) -> str:
227
+ direct = " ".join(prompt_parts or [])
228
+ has_direct = bool(direct)
229
+ has_prompt_file = prompt_file is not None
230
+ stdin_text = read_stdin_source(stdin, block=not (has_direct or has_prompt_file))
231
+ has_stdin = stdin_text is not None
232
+ if sum(1 for present in (has_direct, has_prompt_file, has_stdin) if present) > 1:
233
+ raise DelegateError(
234
+ "ambiguous_prompt_source",
235
+ "Use exactly one prompt source: direct args, --prompt-file, or stdin.",
236
+ )
237
+ if has_direct:
238
+ return validate_prompt(direct)
239
+ if has_prompt_file:
240
+ prompt_file_path = prompt_file
241
+ if prompt_file_path is None:
242
+ raise DelegateError("missing_prompt_file", "--prompt-file requires a path.")
243
+ _reject_windows_path(prompt_file_path, "--prompt-file")
244
+ path = Path(prompt_file_path).expanduser()
245
+ try:
246
+ return validate_prompt(path.read_text(encoding="utf-8"))
247
+ except FileNotFoundError:
248
+ raise DelegateError("prompt_file_not_found", f"Prompt file not found: {path}") from None
249
+ if has_stdin:
250
+ stdin_prompt = stdin_text
251
+ if stdin_prompt is None:
252
+ raise DelegateError("missing_prompt", "Missing stdin prompt.")
253
+ return validate_prompt(stdin_prompt)
254
+ raise DelegateError(
255
+ "missing_prompt", "Missing prompt; pass prompt text, --prompt-file, or stdin."
256
+ )
257
+
258
+
259
+ def read_stdin_source(stdin: TextIO, *, block: bool = False) -> str | None:
260
+ if stdin.isatty():
261
+ return None
262
+ if not block:
263
+ try:
264
+ ready, _, _ = select.select([stdin], [], [], 0)
265
+ if not ready:
266
+ return None
267
+ except (AttributeError, OSError, ValueError):
268
+ if not isinstance(stdin, io.StringIO):
269
+ return None
270
+ data = stdin.read()
271
+ return data if data else None
272
+
273
+
274
+ def validate_prompt(prompt: str) -> str:
275
+ cleaned = "".join(ch for ch in prompt if ord(ch) >= 0x20 or ch in ("\n", "\r", "\t"))
276
+ if not cleaned.strip():
277
+ raise DelegateError("empty_prompt", "Prompt is empty.")
278
+ return cleaned
279
+
280
+
281
+ def resolve_output_schema(engine: str, output_schema: object) -> str | None:
282
+ if output_schema is None:
283
+ return None
284
+ if engine == "grok":
285
+ raise DelegateError(
286
+ "unsupported_output_schema",
287
+ "Grok --json-schema forces final json output, which breaks Delegate tracked "
288
+ "streaming snapshots; --output-schema is not supported for grok in v1.",
289
+ )
290
+ if engine != "codex":
291
+ raise DelegateError(
292
+ "unsupported_output_schema",
293
+ "--output-schema/outputSchema is only supported by the codex engine; "
294
+ f"{engine} has no native schema enforcement.",
295
+ )
296
+ if not isinstance(output_schema, str) or not output_schema:
297
+ raise DelegateError("invalid_output_schema", "outputSchema must be a non-empty string.")
298
+ _reject_windows_path(output_schema, "outputSchema")
299
+ path = Path(output_schema).expanduser()
300
+ if not path.is_absolute():
301
+ path = Path.cwd() / path
302
+ path = path.resolve()
303
+ if not path.exists():
304
+ raise DelegateError("output_schema_not_found", f"Output schema not found: {path}")
305
+ if not path.is_file():
306
+ raise DelegateError("invalid_output_schema", f"Output schema is not a file: {path}")
307
+ try:
308
+ with path.open("rb"):
309
+ pass
310
+ except OSError as exc:
311
+ raise DelegateError(
312
+ "invalid_output_schema", f"Output schema is not readable: {path}"
313
+ ) from exc
314
+ return str(path)
315
+
316
+
317
+ def _completion_report_prompt_mode(
318
+ completion_report_mode: str,
319
+ output_schema: str | None,
320
+ ) -> tuple[str, tuple[str, ...]]:
321
+ if (
322
+ output_schema is not None
323
+ and completion_report_mode == delegate_config.COMPLETION_REPORT_MODE_MARKDOWN
324
+ ):
325
+ return (
326
+ delegate_config.COMPLETION_REPORT_MODE_NONE,
327
+ (OUTPUT_SCHEMA_COMPLETION_REPORT_WARNING,),
328
+ )
329
+ return completion_report_mode, ()
330
+
331
+
332
+ def resolve_completion_report_mode(parsed: ParsedCommand, config: JsonObject) -> str:
333
+ global_options = parsed.global_options
334
+ if global_options.pass_through:
335
+ return delegate_config.COMPLETION_REPORT_MODE_NONE
336
+ if global_options.completion_report is not None:
337
+ return global_options.completion_report
338
+ return delegate_config.completion_report_default_mode(config)
339
+
340
+
341
+ def effective_prompt(
342
+ prompt: str,
343
+ *,
344
+ engine: str = "",
345
+ mode: str = "",
346
+ completion_report_mode: str,
347
+ ) -> str:
348
+ if mode == MODE_CALL:
349
+ return prompt
350
+ prompt = delegate_runner.prepend_skill_review_instructions(prompt)
351
+ safe_prefix = (
352
+ SAFE_REVIEW_PREFIX_BY_ENGINE.get(engine)
353
+ if engine in SAFE_REVIEW_PREFIX_INJECTED_HERE_ENGINES
354
+ else None
355
+ )
356
+ if mode == MODE_SAFE and safe_prefix is not None and safe_prefix not in prompt:
357
+ # prepend_skill_review_instructions guarantees SKILL_REVIEW_PREFIX at index 0,
358
+ # so provider-specific safe prefixes slot in cleanly between skill-review
359
+ # and the user prompt.
360
+ insert_at = len(delegate_runner.SKILL_REVIEW_PREFIX)
361
+ prompt = prompt[:insert_at] + safe_prefix + prompt[insert_at:]
362
+ if completion_report_mode == delegate_config.COMPLETION_REPORT_MODE_MARKDOWN:
363
+ return delegate_runner.append_completion_report_instructions(prompt)
364
+ return prompt
365
+
366
+
367
+ def _safe_dirty_tree_note(
368
+ resolved: ResolvedWorkspace,
369
+ mode: str,
370
+ isolation_context: IsolationContext | None,
371
+ ) -> str | None:
372
+ if mode != MODE_SAFE or resolved.kind != "git":
373
+ return None
374
+ if isolation_context is None or isolation_context.effective_isolation != "worktree":
375
+ return None
376
+ git_root = isolation_context.source_git_root
377
+ if git_root is None or not safe_workspace.git_head_exists(git_root):
378
+ return None
379
+ # ponytail: one bounded git status probe per safe review; cache if this ever shows up hot.
380
+ changed = safe_workspace.changed_files_vs_head(git_root)
381
+ if not changed:
382
+ return None
383
+ shown = [f"`{path}`" for path in changed[:20]]
384
+ remaining = len(changed) - len(shown)
385
+ if remaining > 0:
386
+ shown.append(f"+{remaining} more")
387
+ return (
388
+ f"Note: {len(changed)} file(s) have uncommitted/untracked changes synced into "
389
+ f"this review copy: {', '.join(shown)}. Run `git diff HEAD` and check "
390
+ "untracked files in the workspace to see them."
391
+ )
392
+
393
+
394
+ def _append_safe_dirty_tree_note(
395
+ prompt: str,
396
+ resolved: ResolvedWorkspace,
397
+ mode: str,
398
+ isolation_context: IsolationContext | None,
399
+ ) -> str:
400
+ note = _safe_dirty_tree_note(resolved, mode, isolation_context)
401
+ if note is None:
402
+ return prompt
403
+ return f"{prompt}\n\n{note}"
404
+
405
+
406
+ def _validate_forbid_commit(
407
+ *,
408
+ forbid_commit: bool,
409
+ mode: str,
410
+ isolation_context: IsolationContext | None,
411
+ ) -> None:
412
+ if not forbid_commit:
413
+ return
414
+ if mode != MODE_WORK:
415
+ raise DelegateError(
416
+ "invalid_option_combination",
417
+ "--forbid-commit requires work mode with persistent worktree isolation.",
418
+ )
419
+ source_workspace = (
420
+ isolation_context.source_workspace
421
+ if isolation_context is not None
422
+ else "the selected workspace"
423
+ )
424
+ if isolation_context is not None and isolation_context.source_git_root is None:
425
+ raise DelegateError(
426
+ "invalid_option_combination",
427
+ "--forbid-commit needs worktree isolation, which requires a Git workspace; "
428
+ f"{source_workspace} is not a Git repo, so no-commit enforcement isn't "
429
+ "available here. Omit --forbid-commit (the child may commit), or run "
430
+ "from a Git workspace.",
431
+ )
432
+ if isolation_context is None or isolation_context.isolation_lifecycle != "persistent":
433
+ raise DelegateError(
434
+ "invalid_option_combination",
435
+ "--forbid-commit requires --isolation worktree so Delegate can enforce "
436
+ "the policy — add --isolation worktree, or omit --forbid-commit.",
437
+ )
438
+
439
+
440
+ def _forbid_commit_implied_isolation_note() -> str:
441
+ """The single source of the --forbid-commit implies --isolation worktree note."""
442
+ return "note: --forbid-commit implies --isolation worktree"
443
+
444
+
445
+ def _apply_forbid_commit_isolation_implication(
446
+ *,
447
+ forbid_commit: bool,
448
+ mode: str,
449
+ cli_isolation: str | None,
450
+ json_isolation: str | None,
451
+ ) -> tuple[str | None, str | None, bool]:
452
+ """Apply the forbid-commit isolation implication shared by CLI and input-json paths.
453
+
454
+ Returns ``(resolved_json_isolation, note, implied)``:
455
+ - When forbid-commit is active in work mode with no explicit isolation, the
456
+ implied worktree isolation is returned for the JSON path (the CLI path
457
+ sets this in the parser), plus the note, and ``implied=True``.
458
+ - When forbid-commit is active in work mode with explicit ``none``,
459
+ an ``invalid_option_combination`` error is raised (both paths share this).
460
+ - Otherwise the inputs are returned unchanged with ``implied=False``.
461
+
462
+ This is the single place that owns the implication so ``run --input-json``
463
+ with ``forbidCommit: true`` and no isolation gets the same implied worktree
464
+ isolation + note as the CLI path.
465
+ """
466
+ if not forbid_commit or mode != MODE_WORK:
467
+ return json_isolation, None, False
468
+ effective = cli_isolation if cli_isolation is not None else json_isolation
469
+ if effective is None:
470
+ return "worktree", _forbid_commit_implied_isolation_note(), True
471
+ if effective == "none":
472
+ raise DelegateError(
473
+ "invalid_option_combination",
474
+ "--forbid-commit cannot be combined with --isolation none. "
475
+ "Use --isolation worktree, or omit --forbid-commit.",
476
+ )
477
+ return json_isolation, None, False
478
+
479
+
480
+ def _validate_droid_model_alias(config: JsonObject, model_alias: str | None) -> None:
481
+ models = config["droid"]["models"]
482
+ if model_alias is None or model_alias not in models:
483
+ raise DelegateError("invalid_alias", f"Unknown Droid model alias: {model_alias}")
484
+
485
+
486
+ def _validate_include_dirty(
487
+ *,
488
+ include_dirty: bool,
489
+ mode: str,
490
+ isolation_context: IsolationContext | None,
491
+ ) -> None:
492
+ if not include_dirty:
493
+ return
494
+ if mode != MODE_WORK or isolation_context is None:
495
+ raise DelegateError(
496
+ "invalid_option_combination",
497
+ "--include-dirty requires work mode with --isolation worktree.",
498
+ )
499
+ if isolation_context.isolation_lifecycle != "persistent":
500
+ raise DelegateError(
501
+ "invalid_option_combination",
502
+ "--include-dirty requires work mode with --isolation worktree.",
503
+ )
504
+
505
+
506
+ def _call_workspace(dry_run: bool) -> tuple[ResolvedWorkspace, bool]:
507
+ if dry_run:
508
+ return ResolvedWorkspace(CALL_TEMP_CWD_PLACEHOLDER, "directory"), False
509
+ temp_dir = tempfile.mkdtemp(prefix="delegate-call-")
510
+ return ResolvedWorkspace(temp_dir, "directory"), True
511
+
512
+
513
+ def _validate_call_cli_options(global_options: object, launch: object) -> None:
514
+ cwd = getattr(global_options, "cwd", None)
515
+ isolation = getattr(global_options, "isolation", None)
516
+ pass_through = getattr(global_options, "pass_through", False)
517
+ completion_report = getattr(global_options, "completion_report", None)
518
+ progress_intent = getattr(launch, "progress_intent", None)
519
+ forbid_commit = getattr(launch, "forbid_commit", False)
520
+ include_dirty = getattr(launch, "include_dirty", False)
521
+ if cwd is not None:
522
+ raise DelegateError("invalid_option_combination", "call mode does not use --cwd.")
523
+ if isolation is not None:
524
+ raise DelegateError("invalid_option_combination", "call mode does not use --isolation.")
525
+ if pass_through:
526
+ raise DelegateError(
527
+ "invalid_option_combination",
528
+ "--pass-through is not supported with call mode; call already returns synchronously.",
529
+ )
530
+ if completion_report == delegate_config.COMPLETION_REPORT_MODE_MARKDOWN:
531
+ raise DelegateError(
532
+ "invalid_option_combination",
533
+ "--completion-report is not supported with call mode.",
534
+ )
535
+ if progress_intent == "on":
536
+ raise DelegateError(
537
+ "invalid_option_combination", "--progress is not supported with call mode."
538
+ )
539
+ if forbid_commit:
540
+ raise DelegateError(
541
+ "invalid_option_combination",
542
+ "--forbid-commit requires work mode with persistent worktree isolation.",
543
+ )
544
+ if include_dirty:
545
+ raise DelegateError(
546
+ "invalid_option_combination",
547
+ "--include-dirty requires work mode with persistent worktree isolation.",
548
+ )
549
+
550
+
551
+ def _validate_call_input_json_options(
552
+ global_options: object,
553
+ raw: JsonObject,
554
+ *,
555
+ raw_progress_intent: ProgressIntent,
556
+ raw_forbid_commit: bool,
557
+ raw_include_dirty: bool,
558
+ ) -> None:
559
+ if getattr(global_options, "cwd", None) is not None:
560
+ raise DelegateError("invalid_option_combination", "call mode does not use --cwd.")
561
+ if getattr(global_options, "isolation", None) is not None:
562
+ raise DelegateError("invalid_option_combination", "call mode does not use --isolation.")
563
+ if getattr(global_options, "pass_through", False):
564
+ raise DelegateError(
565
+ "invalid_option_combination",
566
+ "--pass-through is not supported with call mode; call already returns synchronously.",
567
+ )
568
+ if (
569
+ getattr(global_options, "completion_report", None)
570
+ == delegate_config.COMPLETION_REPORT_MODE_MARKDOWN
571
+ ):
572
+ raise DelegateError(
573
+ "invalid_option_combination",
574
+ "--completion-report is not supported with call mode.",
575
+ )
576
+ if raw.get("cwd") is not None:
577
+ raise DelegateError(
578
+ "invalid_option_combination", "call mode input JSON must not include cwd."
579
+ )
580
+ if "isolation" in raw:
581
+ raise DelegateError(
582
+ "invalid_option_combination",
583
+ "call mode input JSON must not include isolation.",
584
+ )
585
+ if raw_progress_intent == "on":
586
+ raise DelegateError(
587
+ "invalid_option_combination", "progress is not supported with call mode."
588
+ )
589
+ if raw_forbid_commit:
590
+ raise DelegateError(
591
+ "invalid_option_combination",
592
+ "forbidCommit requires work mode with persistent worktree isolation.",
593
+ )
594
+ if raw_include_dirty:
595
+ raise DelegateError(
596
+ "invalid_option_combination",
597
+ "includeDirty requires work mode with persistent worktree isolation.",
598
+ )
599
+
600
+
601
+ def _safe_none_normalization_warnings(
602
+ *,
603
+ engine: str,
604
+ mode: str,
605
+ requested: str | None,
606
+ effective: str,
607
+ ) -> tuple[str, ...]:
608
+ if (
609
+ mode == MODE_SAFE
610
+ and requested == delegate_config.ISOLATION_NONE
611
+ and effective == delegate_config.ISOLATION_AUTO
612
+ and engine in delegate_config.SAFE_ISOLATION_REQUIRED_ENGINES
613
+ ):
614
+ return (
615
+ f"isolation none is not used for {engine} safe mode; using auto "
616
+ "temporary isolation instead.",
617
+ )
618
+ return ()
619
+
620
+
621
+ def request_from_parsed(parsed: ParsedCommand, config: JsonObject, stdin: TextIO) -> Request:
622
+ validate_config(config)
623
+ if parsed.subcommand == "run":
624
+ return request_from_input_json(parsed, config)
625
+ launch = parsed.launch
626
+ global_options = parsed.global_options
627
+ if launch is None or launch.engine not in KNOWN_ENGINES:
628
+ raise DelegateError("invalid_command", "Command does not map to an execution request.")
629
+ if launch.mode is None:
630
+ raise DelegateError("invalid_command", "Command does not map to an execution request.")
631
+ if launch.mode == MODE_CALL:
632
+ _validate_call_cli_options(global_options, launch)
633
+ read_only = getattr(launch, "read_only", False)
634
+ if launch.engine == "droid":
635
+ _validate_droid_model_alias(config, launch.model_alias)
636
+ output_schema = resolve_output_schema(launch.engine, launch.output_schema)
637
+ prompt = _call_effective_prompt(
638
+ resolve_prompt(launch.prompt_parts, launch.prompt_file, stdin),
639
+ read_only=read_only,
640
+ )
641
+ workspace, cleanup_workspace = _call_workspace(launch.dry_run)
642
+ try:
643
+ return build_request(
644
+ launch.engine,
645
+ launch.mode,
646
+ launch.model_alias,
647
+ workspace,
648
+ prompt,
649
+ config,
650
+ launch.dry_run,
651
+ stream_capture=True,
652
+ isolation_context=None,
653
+ reasoning_effort=launch.reasoning_effort,
654
+ reasoning_effort_source="cli" if launch.reasoning_effort is not None else None,
655
+ progress=False,
656
+ forbid_commit=False,
657
+ auth_profile_override=global_options.auth_profile,
658
+ output_schema=output_schema,
659
+ cleanup_workspace=cleanup_workspace,
660
+ call_read_only=read_only,
661
+ group=global_options.group,
662
+ )
663
+ except BaseException:
664
+ if cleanup_workspace:
665
+ shutil.rmtree(workspace.path, ignore_errors=True)
666
+ raise
667
+ if getattr(launch, "read_only", False):
668
+ raise DelegateError(
669
+ "invalid_option_combination",
670
+ "--read-only only applies to call mode.",
671
+ )
672
+ effective_progress = resolve_effective_progress(launch.progress_intent, config)
673
+ if effective_progress and global_options.pass_through:
674
+ raise DelegateError(
675
+ "invalid_option_combination",
676
+ "--progress is incompatible with --pass-through.",
677
+ )
678
+ progress_initial_delay_sec, progress_interval_sec = resolve_progress_timing(config)
679
+ output_schema = resolve_output_schema(launch.engine, launch.output_schema)
680
+ workspace = resolve_workspace(global_options.cwd)
681
+ prompt = resolve_prompt(launch.prompt_parts, launch.prompt_file, stdin)
682
+
683
+ # Capture git metadata for isolation planning (read-only, safe in dry-run too).
684
+ git_root, git_common_dir, git_head_oid, git_head_ref, git_branch = capture_git_metadata(
685
+ workspace.path
686
+ )
687
+
688
+ # Resolve effective isolation and build isolation context.
689
+ try:
690
+ effective_isolation = delegate_config.resolve_isolation(
691
+ cli_value=global_options.isolation,
692
+ loaded_config=config,
693
+ engine=launch.engine,
694
+ mode=launch.mode,
695
+ )
696
+ except delegate_config.InvalidIsolationError as exc:
697
+ raise DelegateError("invalid_isolation", str(exc)) from exc
698
+ isolation_warnings = list(
699
+ _safe_none_normalization_warnings(
700
+ engine=launch.engine,
701
+ mode=launch.mode,
702
+ requested=global_options.isolation,
703
+ effective=effective_isolation,
704
+ )
705
+ )
706
+ if getattr(launch, "forbid_commit_implied_isolation", False):
707
+ isolation_warnings.append(_forbid_commit_implied_isolation_note())
708
+
709
+ isolation_context = build_isolation_context(
710
+ source_workspace=workspace.path,
711
+ resolved_isolation=effective_isolation,
712
+ engine=launch.engine,
713
+ mode=launch.mode,
714
+ model_alias=launch.model_alias,
715
+ config=config,
716
+ run_short_id="<short-run-id-placeholder>" if launch.dry_run else None,
717
+ source_git_root=git_root,
718
+ source_git_common_dir=git_common_dir,
719
+ source_head_oid=git_head_oid,
720
+ source_head_ref=git_head_ref,
721
+ source_branch=git_branch,
722
+ include_dirty=launch.include_dirty,
723
+ )
724
+ _validate_forbid_commit(
725
+ forbid_commit=launch.forbid_commit,
726
+ mode=launch.mode,
727
+ isolation_context=isolation_context,
728
+ )
729
+ _validate_include_dirty(
730
+ include_dirty=launch.include_dirty,
731
+ mode=launch.mode,
732
+ isolation_context=isolation_context,
733
+ )
734
+
735
+ completion_report_mode = resolve_completion_report_mode(parsed, config)
736
+ completion_report_prompt_mode, output_schema_warnings = _completion_report_prompt_mode(
737
+ completion_report_mode,
738
+ output_schema,
739
+ )
740
+ prompt = effective_prompt(
741
+ prompt,
742
+ engine=launch.engine,
743
+ mode=launch.mode,
744
+ completion_report_mode=completion_report_prompt_mode,
745
+ )
746
+ return build_request(
747
+ launch.engine,
748
+ launch.mode,
749
+ launch.model_alias,
750
+ workspace,
751
+ prompt,
752
+ config,
753
+ launch.dry_run,
754
+ stream_capture=not global_options.pass_through,
755
+ isolation_context=isolation_context,
756
+ reasoning_effort=launch.reasoning_effort,
757
+ reasoning_effort_source="cli" if launch.reasoning_effort is not None else None,
758
+ progress=effective_progress,
759
+ progress_initial_delay_sec=progress_initial_delay_sec,
760
+ progress_interval_sec=progress_interval_sec,
761
+ forbid_commit=launch.forbid_commit,
762
+ include_dirty=launch.include_dirty,
763
+ auth_profile_override=global_options.auth_profile,
764
+ output_schema=output_schema,
765
+ warnings=(*output_schema_warnings, *isolation_warnings),
766
+ group=global_options.group,
767
+ )
768
+
769
+
770
+ def _load_input_json_object(path: Path) -> JsonObject:
771
+ try:
772
+ raw: JsonValue = json.loads(path.read_text(encoding="utf-8"))
773
+ except FileNotFoundError:
774
+ raise DelegateError("input_json_not_found", f"Input JSON file not found: {path}") from None
775
+ except json.JSONDecodeError as exc:
776
+ raise DelegateError("invalid_input_json", f"Invalid input JSON: {exc}") from exc
777
+ if not isinstance(raw, dict):
778
+ raise DelegateError("invalid_input_json", "Input JSON root must be an object.")
779
+ return raw
780
+
781
+
782
+ def request_from_input_json(parsed: ParsedCommand, config: JsonObject) -> Request:
783
+ run_json = parsed.run_json
784
+ if run_json is None:
785
+ raise DelegateError("invalid_command", "run --input-json options are required.")
786
+ global_options = parsed.global_options
787
+ _reject_windows_path(run_json.input_json, "--input-json")
788
+ path = Path(run_json.input_json).expanduser()
789
+ raw = _load_input_json_object(path)
790
+
791
+ if "profile" in raw:
792
+ raise DelegateError(
793
+ "invalid_input_key",
794
+ "Input JSON must not include profile; set codex.profile in config for Codex.",
795
+ )
796
+ unknown = sorted(set(raw) - RUN_INPUT_KEYS)
797
+ if unknown:
798
+ raise DelegateError("unknown_input_key", f"Unknown input JSON keys: {', '.join(unknown)}")
799
+ engine = raw.get("engine")
800
+ mode = raw.get("mode")
801
+ prompt = raw.get("prompt")
802
+ if engine not in KNOWN_ENGINES:
803
+ raise DelegateError(
804
+ "invalid_engine",
805
+ f"engine must be {ENGINES_PROSE}.",
806
+ )
807
+ if not isinstance(mode, str):
808
+ raise DelegateError("invalid_mode", "mode must be safe, work, or call.")
809
+ validate_mode(mode)
810
+ if not isinstance(prompt, str):
811
+ raise DelegateError("invalid_prompt", "prompt must be a string.")
812
+ model_alias = raw.get("model")
813
+ raw_reasoning_effort = raw.get("reasoningEffort")
814
+ if raw_reasoning_effort is not None:
815
+ try:
816
+ reasoning_effort = reasoning.normalize_effort(raw_reasoning_effort)
817
+ except reasoning.ReasoningCapabilityError as exc:
818
+ raise DelegateError(exc.error, "reasoningEffort must be a non-empty string.") from exc
819
+ else:
820
+ reasoning_effort = None
821
+ raw_progress_intent: ProgressIntent
822
+ if "progress" in raw:
823
+ raw_progress = raw["progress"]
824
+ if not isinstance(raw_progress, bool):
825
+ raise DelegateError("invalid_progress", "progress must be true or false.")
826
+ raw_progress_intent = "on" if raw_progress else "off"
827
+ else:
828
+ raw_progress_intent = None
829
+ effective_progress = resolve_effective_progress(raw_progress_intent, config)
830
+ if effective_progress and global_options.pass_through:
831
+ raise DelegateError(
832
+ "invalid_option_combination",
833
+ "progress is incompatible with --pass-through.",
834
+ )
835
+ progress_initial_delay_sec, progress_interval_sec = resolve_progress_timing(config)
836
+ raw_forbid_commit = raw.get("forbidCommit", False)
837
+ if not isinstance(raw_forbid_commit, bool):
838
+ raise DelegateError("invalid_forbid_commit", "forbidCommit must be true or false.")
839
+ raw_read_only = raw.get("readOnly", False)
840
+ if not isinstance(raw_read_only, bool):
841
+ raise DelegateError("invalid_read_only", "readOnly must be true or false.")
842
+ if raw_read_only and mode != MODE_CALL:
843
+ raise DelegateError(
844
+ "invalid_option_combination",
845
+ "readOnly only applies to call mode.",
846
+ )
847
+ raw_include_dirty = raw.get("includeDirty", False)
848
+ if not isinstance(raw_include_dirty, bool):
849
+ raise DelegateError("invalid_include_dirty", "includeDirty must be true or false.")
850
+ if engine == "droid":
851
+ if not isinstance(model_alias, str) or not model_alias:
852
+ raise DelegateError("missing_model", "droid run input requires model alias.")
853
+ elif engine in MODELESS_NONCURSOR_ENGINES:
854
+ if model_alias is not None and not isinstance(model_alias, str):
855
+ raise DelegateError("invalid_model", f"model must be a string or null for {engine}.")
856
+ if model_alias == "":
857
+ raise DelegateError(
858
+ "invalid_model", f"model must be a non-empty string or omitted for {engine}."
859
+ )
860
+ elif model_alias is not None and model_alias != config["cursor"]["defaultModel"]:
861
+ raise DelegateError(
862
+ "invalid_model", "cursor model override must match configured Composer model."
863
+ )
864
+ output_schema = resolve_output_schema(str(engine), raw.get("outputSchema"))
865
+
866
+ if mode == MODE_CALL:
867
+ _validate_call_input_json_options(
868
+ global_options,
869
+ raw,
870
+ raw_progress_intent=raw_progress_intent,
871
+ raw_forbid_commit=raw_forbid_commit,
872
+ raw_include_dirty=raw_include_dirty,
873
+ )
874
+ if engine == "droid":
875
+ _validate_droid_model_alias(config, model_alias)
876
+ workspace, cleanup_workspace = _call_workspace(False)
877
+ try:
878
+ return build_request(
879
+ str(engine),
880
+ str(mode),
881
+ model_alias,
882
+ workspace,
883
+ _call_effective_prompt(validate_prompt(prompt), read_only=raw_read_only),
884
+ config,
885
+ dry_run=False,
886
+ stream_capture=True,
887
+ isolation_context=None,
888
+ reasoning_effort=reasoning_effort,
889
+ reasoning_effort_source="input-json" if reasoning_effort is not None else None,
890
+ progress=False,
891
+ forbid_commit=False,
892
+ auth_profile_override=global_options.auth_profile,
893
+ output_schema=output_schema,
894
+ cleanup_workspace=cleanup_workspace,
895
+ call_read_only=raw_read_only,
896
+ group=global_options.group,
897
+ )
898
+ except BaseException:
899
+ if cleanup_workspace:
900
+ shutil.rmtree(workspace.path, ignore_errors=True)
901
+ raise
902
+
903
+ # Pre-read cwd and isolation from JSON for config discovery (already done in main() for
904
+ # config loading, but re-validate and resolve here for the request).
905
+ json_cwd = raw.get("cwd")
906
+ if json_cwd is not None and not isinstance(json_cwd, str):
907
+ raise DelegateError("invalid_cwd", "cwd must be a string.")
908
+
909
+ # Reject explicit null isolation in the JSON (distinguish missing-key from null).
910
+ if "isolation" in raw and raw["isolation"] is None:
911
+ raise DelegateError(
912
+ "invalid_isolation",
913
+ "isolation in input JSON must be auto, none, or worktree (null is not allowed).",
914
+ )
915
+ json_isolation = raw.get("isolation")
916
+ if json_isolation is not None and json_isolation not in delegate_config.VALID_ISOLATION_VALUES:
917
+ raise DelegateError(
918
+ "invalid_isolation",
919
+ "isolation in input JSON must be auto, none, or worktree.",
920
+ )
921
+
922
+ # Apply the forbid-commit isolation implication shared with the CLI path so
923
+ # run --input-json with forbidCommit: true and no isolation gets the same
924
+ # implied worktree isolation + note. Explicit "none" + forbidCommit errors
925
+ # here (both paths share this refusal).
926
+ forbid_commit_note: str | None = None
927
+ if raw_forbid_commit:
928
+ json_isolation, forbid_commit_note, _ = _apply_forbid_commit_isolation_implication(
929
+ forbid_commit=raw_forbid_commit,
930
+ mode=str(mode),
931
+ cli_isolation=global_options.isolation,
932
+ json_isolation=json_isolation,
933
+ )
934
+
935
+ workspace = resolve_workspace(global_options.cwd, json_cwd)
936
+ git_root, git_common_dir, git_head_oid, git_head_ref, git_branch = capture_git_metadata(
937
+ workspace.path
938
+ )
939
+
940
+ try:
941
+ effective_isolation = delegate_config.resolve_isolation(
942
+ cli_value=global_options.isolation,
943
+ input_json_value=json_isolation,
944
+ loaded_config=config,
945
+ engine=str(engine),
946
+ mode=str(mode),
947
+ )
948
+ except delegate_config.InvalidIsolationError as exc:
949
+ raise DelegateError("invalid_isolation", str(exc)) from exc
950
+ isolation_warnings = list(
951
+ _safe_none_normalization_warnings(
952
+ engine=str(engine),
953
+ mode=str(mode),
954
+ requested=global_options.isolation or json_isolation,
955
+ effective=effective_isolation,
956
+ )
957
+ )
958
+ if forbid_commit_note is not None:
959
+ isolation_warnings.append(forbid_commit_note)
960
+
961
+ isolation_context = build_isolation_context(
962
+ source_workspace=workspace.path,
963
+ resolved_isolation=effective_isolation,
964
+ engine=str(engine),
965
+ mode=str(mode),
966
+ model_alias=model_alias,
967
+ config=config,
968
+ run_short_id=None,
969
+ source_git_root=git_root,
970
+ source_git_common_dir=git_common_dir,
971
+ source_head_oid=git_head_oid,
972
+ source_head_ref=git_head_ref,
973
+ source_branch=git_branch,
974
+ include_dirty=raw_include_dirty,
975
+ )
976
+ _validate_forbid_commit(
977
+ forbid_commit=raw_forbid_commit,
978
+ mode=str(mode),
979
+ isolation_context=isolation_context,
980
+ )
981
+ _validate_include_dirty(
982
+ include_dirty=raw_include_dirty,
983
+ mode=str(mode),
984
+ isolation_context=isolation_context,
985
+ )
986
+ if engine == "droid":
987
+ _validate_droid_model_alias(config, model_alias)
988
+
989
+ completion_report_mode = resolve_completion_report_mode(parsed, config)
990
+ completion_report_prompt_mode, output_schema_warnings = _completion_report_prompt_mode(
991
+ completion_report_mode,
992
+ output_schema,
993
+ )
994
+ prompt = effective_prompt(
995
+ validate_prompt(prompt),
996
+ engine=str(engine),
997
+ mode=str(mode),
998
+ completion_report_mode=completion_report_prompt_mode,
999
+ )
1000
+ return build_request(
1001
+ str(engine),
1002
+ str(mode),
1003
+ model_alias,
1004
+ workspace,
1005
+ prompt,
1006
+ config,
1007
+ dry_run=False,
1008
+ stream_capture=not global_options.pass_through,
1009
+ isolation_context=isolation_context,
1010
+ reasoning_effort=reasoning_effort,
1011
+ reasoning_effort_source="input-json" if reasoning_effort is not None else None,
1012
+ progress=effective_progress,
1013
+ progress_initial_delay_sec=progress_initial_delay_sec,
1014
+ progress_interval_sec=progress_interval_sec,
1015
+ forbid_commit=raw_forbid_commit,
1016
+ include_dirty=raw_include_dirty,
1017
+ auth_profile_override=global_options.auth_profile,
1018
+ output_schema=output_schema,
1019
+ warnings=(*output_schema_warnings, *isolation_warnings),
1020
+ group=global_options.group,
1021
+ )
1022
+
1023
+
1024
+ def build_request(
1025
+ engine: str,
1026
+ mode: str,
1027
+ model_alias: str | None,
1028
+ workspace: ResolvedWorkspace,
1029
+ prompt: str,
1030
+ config: JsonObject,
1031
+ dry_run: bool,
1032
+ *,
1033
+ stream_capture: bool = True,
1034
+ isolation_context: IsolationContext | None = None,
1035
+ reasoning_effort: str | None = None,
1036
+ reasoning_effort_source: str | None = None,
1037
+ progress: bool = False,
1038
+ progress_initial_delay_sec: float = delegate_runner.PROGRESS_INITIAL_DELAY_SEC,
1039
+ progress_interval_sec: float = delegate_runner.PROGRESS_HEARTBEAT_INTERVAL_SEC,
1040
+ forbid_commit: bool = False,
1041
+ include_dirty: bool = False,
1042
+ auth_profile_override: str | None = None,
1043
+ output_schema: str | None = None,
1044
+ warnings: tuple[str, ...] = (),
1045
+ cleanup_workspace: bool = False,
1046
+ call_read_only: bool = False,
1047
+ group: str | None = None,
1048
+ ) -> Request:
1049
+ if not isinstance(workspace, ResolvedWorkspace):
1050
+ raise TypeError(
1051
+ "build_request requires a ResolvedWorkspace; "
1052
+ "wrap raw Git workspace paths with ResolvedWorkspace(path, 'git')."
1053
+ )
1054
+ requested_effort, effort_source = resolve_effective_reasoning_effort(
1055
+ config,
1056
+ engine,
1057
+ reasoning_effort,
1058
+ reasoning_effort_source=reasoning_effort_source,
1059
+ )
1060
+ output_schema = resolve_output_schema(engine, output_schema)
1061
+
1062
+ return _build_request_for_workspace(
1063
+ engine,
1064
+ mode,
1065
+ model_alias,
1066
+ workspace,
1067
+ prompt,
1068
+ config,
1069
+ dry_run,
1070
+ stream_capture=stream_capture,
1071
+ isolation_context=isolation_context,
1072
+ requested_effort=requested_effort,
1073
+ effort_source=effort_source,
1074
+ progress=progress,
1075
+ progress_initial_delay_sec=progress_initial_delay_sec,
1076
+ progress_interval_sec=progress_interval_sec,
1077
+ forbid_commit=forbid_commit,
1078
+ include_dirty=include_dirty,
1079
+ auth_profile_override=auth_profile_override,
1080
+ output_schema=output_schema,
1081
+ warnings=warnings,
1082
+ cleanup_workspace=cleanup_workspace,
1083
+ call_read_only=call_read_only,
1084
+ group=group,
1085
+ )
1086
+
1087
+
1088
+ def _cursor_request_parts(build: EngineBuildInput) -> EngineRequestParts:
1089
+ _ = build.model_alias, build.cache
1090
+ cursor = build.config["cursor"]
1091
+ capability, reasoning_warnings = _capability_with_config_fallback(
1092
+ lambda: resolve_cursor_reasoning_capability(cursor, build.requested_effort),
1093
+ engine="cursor",
1094
+ effort_source=build.effort_source,
1095
+ )
1096
+ model = capability.model if capability is not None else cursor["defaultModel"]
1097
+ argv = build_cursor_argv(
1098
+ cursor["argvPrefix"],
1099
+ build.mode,
1100
+ build.resolved.path,
1101
+ model,
1102
+ build.prompt,
1103
+ stream_capture=build.stream_capture,
1104
+ call_read_only=build.call_read_only,
1105
+ )
1106
+ return EngineRequestParts(
1107
+ model=model,
1108
+ argv=argv,
1109
+ model_alias=None,
1110
+ prompt_transport=PROMPT_TRANSPORT_ARGV,
1111
+ display_argv=redacted_prompt_argv(argv),
1112
+ warnings=reasoning_warnings,
1113
+ **reasoning_request_kwargs(capability, build.effort_source),
1114
+ )
1115
+
1116
+
1117
+ def _droid_request_parts(build: EngineBuildInput) -> EngineRequestParts:
1118
+ droid = build.config["droid"]
1119
+ models = droid["models"]
1120
+ _validate_droid_model_alias(build.config, build.model_alias)
1121
+ model = models[build.model_alias]
1122
+ if model.startswith("replace-with-") or model in {
1123
+ "your-droid-model-id",
1124
+ "real-droid-model-id",
1125
+ }:
1126
+ raise DelegateError(
1127
+ "unconfigured_model",
1128
+ (
1129
+ f"Droid model alias '{build.model_alias}' is still a placeholder. "
1130
+ "Copy config.example.json to ~/.delegate/config.json and set a real Droid model ID."
1131
+ ),
1132
+ )
1133
+ capability, reasoning_warnings = _capability_with_config_fallback(
1134
+ lambda: reasoning.resolve_reasoning_capability(
1135
+ harness="droid",
1136
+ model=model,
1137
+ requested_effort=build.requested_effort,
1138
+ config=build.config,
1139
+ cache=build.cache,
1140
+ alias=build.model_alias,
1141
+ ),
1142
+ engine="droid",
1143
+ effort_source=build.effort_source,
1144
+ )
1145
+ prompt = build.prompt
1146
+ if build.mode == MODE_SAFE:
1147
+ prompt = prefix_droid_safe_prompt(prompt)
1148
+ argv = build_droid_argv(
1149
+ droid["binary"],
1150
+ build.mode,
1151
+ build.resolved.path,
1152
+ model,
1153
+ prompt,
1154
+ stream_capture=build.stream_capture,
1155
+ reasoning_capability=capability,
1156
+ prompt_transport=PROMPT_TRANSPORT_FILE,
1157
+ call_read_only=build.call_read_only,
1158
+ )
1159
+ display_argv = [
1160
+ DROID_PROMPT_FILE_DISPLAY if item == DROID_PROMPT_FILE_ARG_PLACEHOLDER else item
1161
+ for item in argv
1162
+ ]
1163
+ return EngineRequestParts(
1164
+ model=model,
1165
+ argv=argv,
1166
+ model_alias=build.model_alias,
1167
+ prompt_transport=PROMPT_TRANSPORT_FILE,
1168
+ prompt_file_text=prompt,
1169
+ display_argv=display_argv,
1170
+ warnings=reasoning_warnings,
1171
+ **reasoning_request_kwargs(capability, build.effort_source),
1172
+ )
1173
+
1174
+
1175
+ def _codex_request_parts(build: EngineBuildInput) -> EngineRequestParts:
1176
+ codex = build.config["codex"]
1177
+ if isinstance(build.model_alias, str) and build.model_alias:
1178
+ model = build.model_alias
1179
+ else:
1180
+ model = _resolve_default_model(codex)
1181
+ policy = delegate_config.effective_policy(
1182
+ build.config, engine="codex", mode=_policy_mode(build)
1183
+ )
1184
+ if model is None and build.requested_effort is not None and build.effort_source != "config":
1185
+ effort = reasoning.normalize_effort(build.requested_effort)
1186
+ if effort not in CODEX_HARNESS_DEFAULT_REASONING_EFFORTS:
1187
+ raise DelegateError(
1188
+ "unsupported_reasoning_effort",
1189
+ reasoning.format_explicit_reasoning_effort_error(
1190
+ harness="codex",
1191
+ effort=effort,
1192
+ supported=CODEX_HARNESS_DEFAULT_REASONING_EFFORTS,
1193
+ detail="does not support reasoning effort with the harness default model",
1194
+ ),
1195
+ )
1196
+ capability = reasoning.ReasoningCapability(
1197
+ harness="codex",
1198
+ model="",
1199
+ effort=effort,
1200
+ supported_efforts=CODEX_HARNESS_DEFAULT_REASONING_EFFORTS,
1201
+ default_effort=None,
1202
+ transport=reasoning.TRANSPORT_BY_HARNESS["codex"],
1203
+ source="harness-default",
1204
+ )
1205
+ reasoning_warnings = (
1206
+ "codex.defaultModel is unset; applying --reasoning-effort to the Codex "
1207
+ "harness default model.",
1208
+ )
1209
+ else:
1210
+ capability, reasoning_warnings = _capability_with_config_fallback(
1211
+ lambda: reasoning.resolve_reasoning_capability(
1212
+ harness="codex",
1213
+ model=model,
1214
+ requested_effort=build.requested_effort,
1215
+ config=build.config,
1216
+ cache=build.cache,
1217
+ alias=build.model_alias or model,
1218
+ ),
1219
+ engine="codex",
1220
+ effort_source=build.effort_source,
1221
+ )
1222
+ argv = build_codex_argv(
1223
+ codex,
1224
+ build.mode,
1225
+ build.resolved.path,
1226
+ model,
1227
+ build.prompt,
1228
+ policy,
1229
+ workspace_kind=build.resolved.kind,
1230
+ stream_capture=build.stream_capture,
1231
+ reasoning_capability=capability,
1232
+ prompt_transport=PROMPT_TRANSPORT_STDIN,
1233
+ output_schema=build.output_schema,
1234
+ call_read_only=build.call_read_only,
1235
+ )
1236
+ return EngineRequestParts(
1237
+ model=model,
1238
+ argv=argv,
1239
+ model_alias=build.model_alias,
1240
+ prompt_transport=PROMPT_TRANSPORT_STDIN,
1241
+ stdin_text=build.prompt,
1242
+ display_argv=list(argv),
1243
+ warnings=reasoning_warnings,
1244
+ **reasoning_request_kwargs(capability, build.effort_source),
1245
+ )
1246
+
1247
+
1248
+ def _claude_request_parts(build: EngineBuildInput) -> EngineRequestParts:
1249
+ claude = build.config["claude"]
1250
+ if isinstance(build.model_alias, str) and build.model_alias:
1251
+ model = build.model_alias
1252
+ else:
1253
+ model = _resolve_default_model(claude)
1254
+ policy = delegate_config.effective_policy(
1255
+ build.config, engine="claude", mode=_policy_mode(build)
1256
+ )
1257
+ effort: str | None = None
1258
+ warnings: tuple[str, ...] = ()
1259
+ if build.requested_effort is not None:
1260
+ try:
1261
+ effort = reasoning.resolve_claude_native_effort(
1262
+ build.requested_effort,
1263
+ alias=build.model_alias,
1264
+ model=model,
1265
+ )
1266
+ except reasoning.ReasoningCapabilityError as exc:
1267
+ if build.effort_source != "config":
1268
+ raise DelegateError(exc.error, exc.message) from exc
1269
+ warnings = (f"ignoring claude.defaultReasoningEffort: {exc.message}",)
1270
+ argv = build_claude_argv(
1271
+ claude,
1272
+ build.mode,
1273
+ model,
1274
+ policy,
1275
+ stream_capture=build.stream_capture,
1276
+ reasoning_effort=effort,
1277
+ allow_bypass_permissions=_claude_harness_bypass_enabled(build.config, build.mode),
1278
+ call_read_only=build.call_read_only,
1279
+ )
1280
+ return EngineRequestParts(
1281
+ model=model,
1282
+ argv=argv,
1283
+ model_alias=build.model_alias,
1284
+ prompt_transport=PROMPT_TRANSPORT_STDIN,
1285
+ stdin_text=build.prompt,
1286
+ display_argv=list(argv),
1287
+ warnings=warnings,
1288
+ **claude_reasoning_request_kwargs(effort, build.effort_source),
1289
+ )
1290
+
1291
+
1292
+ def _grok_request_parts(build: EngineBuildInput) -> EngineRequestParts:
1293
+ grok = build.config["grok"]
1294
+ if isinstance(build.model_alias, str) and build.model_alias:
1295
+ model = build.model_alias
1296
+ else:
1297
+ model = _resolve_default_model(grok)
1298
+ policy = delegate_config.effective_policy(build.config, engine="grok", mode=_policy_mode(build))
1299
+ effort: str | None = None
1300
+ warnings: tuple[str, ...] = ()
1301
+ if build.requested_effort is not None:
1302
+ try:
1303
+ effort = reasoning.resolve_grok_native_effort(
1304
+ build.requested_effort,
1305
+ alias=build.model_alias,
1306
+ model=model,
1307
+ )
1308
+ except reasoning.ReasoningCapabilityError as exc:
1309
+ if build.effort_source != "config":
1310
+ raise DelegateError(exc.error, exc.message) from exc
1311
+ warnings = (f"ignoring grok.defaultReasoningEffort: {exc.message}",)
1312
+ argv = build_grok_argv(
1313
+ grok,
1314
+ build.mode,
1315
+ build.resolved.path,
1316
+ model,
1317
+ policy,
1318
+ stream_capture=build.stream_capture,
1319
+ reasoning_effort=effort,
1320
+ allow_bypass_permissions=_grok_harness_bypass_enabled(build.config, build.mode),
1321
+ call_read_only=build.call_read_only,
1322
+ )
1323
+ display_argv = [
1324
+ PROMPT_FILE_DISPLAY if item == PROMPT_FILE_ARG_PLACEHOLDER else item for item in argv
1325
+ ]
1326
+ return EngineRequestParts(
1327
+ model=model,
1328
+ argv=argv,
1329
+ model_alias=build.model_alias,
1330
+ prompt_transport=PROMPT_TRANSPORT_FILE,
1331
+ prompt_file_text=build.prompt,
1332
+ display_argv=display_argv,
1333
+ warnings=warnings,
1334
+ **grok_reasoning_request_kwargs(effort, build.effort_source),
1335
+ )
1336
+
1337
+
1338
+ def _kimi_request_parts(build: EngineBuildInput) -> EngineRequestParts:
1339
+ _ = build.effort_source, build.cache
1340
+ if build.requested_effort is not None:
1341
+ kimi = build.config.get("kimi")
1342
+ default_model = kimi.get("defaultModel") if isinstance(kimi, dict) else None
1343
+ model = default_model if isinstance(default_model, str) and default_model else None
1344
+ raise DelegateError(
1345
+ "unsupported_reasoning_effort",
1346
+ reasoning.format_explicit_reasoning_effort_error(
1347
+ harness="kimi",
1348
+ alias=build.model_alias,
1349
+ model=model,
1350
+ effort=build.requested_effort,
1351
+ detail="reasoning effort is not supported",
1352
+ ),
1353
+ )
1354
+ kimi = build.config["kimi"]
1355
+ if isinstance(build.model_alias, str) and build.model_alias:
1356
+ model = build.model_alias
1357
+ else:
1358
+ model = _resolve_default_model(kimi)
1359
+ argv = build_kimi_argv(
1360
+ kimi,
1361
+ build.mode,
1362
+ build.resolved.path,
1363
+ model,
1364
+ build.prompt,
1365
+ stream_capture=build.stream_capture,
1366
+ )
1367
+ return EngineRequestParts(
1368
+ model=model,
1369
+ argv=argv,
1370
+ model_alias=build.model_alias,
1371
+ prompt_transport=PROMPT_TRANSPORT_ARGV,
1372
+ display_argv=redacted_prompt_argv(argv, replacement=KIMI_PROMPT_REDACTION),
1373
+ )
1374
+
1375
+
1376
+ EngineRequestPartsBuilder = Callable[[EngineBuildInput], EngineRequestParts]
1377
+
1378
+ ENGINE_REQUEST_PARTS_BUILDERS: dict[str, EngineRequestPartsBuilder] = {
1379
+ "cursor": _cursor_request_parts,
1380
+ "droid": _droid_request_parts,
1381
+ "codex": _codex_request_parts,
1382
+ "claude": _claude_request_parts,
1383
+ "grok": _grok_request_parts,
1384
+ "kimi": _kimi_request_parts,
1385
+ }
1386
+
1387
+
1388
+ def _engine_request_parts(
1389
+ engine: str,
1390
+ *,
1391
+ build: EngineBuildInput,
1392
+ ) -> EngineRequestParts:
1393
+ builder = ENGINE_REQUEST_PARTS_BUILDERS.get(engine)
1394
+ if builder is None:
1395
+ raise DelegateError(
1396
+ "invalid_engine",
1397
+ f"engine must be {ENGINES_PROSE}.",
1398
+ )
1399
+ return builder(build)
1400
+
1401
+
1402
+ def _build_request_for_workspace(
1403
+ engine: str,
1404
+ mode: str,
1405
+ model_alias: str | None,
1406
+ resolved: ResolvedWorkspace,
1407
+ prompt: str,
1408
+ config: JsonObject,
1409
+ dry_run: bool,
1410
+ *,
1411
+ stream_capture: bool,
1412
+ isolation_context: IsolationContext | None,
1413
+ requested_effort: str | None,
1414
+ effort_source: str | None,
1415
+ progress: bool,
1416
+ progress_initial_delay_sec: float,
1417
+ progress_interval_sec: float,
1418
+ forbid_commit: bool,
1419
+ include_dirty: bool,
1420
+ auth_profile_override: str | None,
1421
+ output_schema: str | None,
1422
+ warnings: tuple[str, ...],
1423
+ cleanup_workspace: bool,
1424
+ call_read_only: bool = False,
1425
+ group: str | None = None,
1426
+ ) -> Request:
1427
+ prompt = _append_safe_dirty_tree_note(prompt, resolved, mode, isolation_context)
1428
+ workspace_warning = wsl.drivefs_workspace_warning(resolved.path)
1429
+ if workspace_warning is not None:
1430
+ warnings = (*warnings, workspace_warning)
1431
+ cache = (
1432
+ reasoning.load_reasoning_capability_cache(resolved.path)
1433
+ if requested_effort is not None
1434
+ else None
1435
+ )
1436
+ parts = _engine_request_parts(
1437
+ engine,
1438
+ build=EngineBuildInput(
1439
+ mode=mode,
1440
+ model_alias=model_alias,
1441
+ resolved=resolved,
1442
+ prompt=prompt,
1443
+ config=config,
1444
+ stream_capture=stream_capture,
1445
+ requested_effort=requested_effort,
1446
+ effort_source=effort_source,
1447
+ cache=cache,
1448
+ output_schema=output_schema,
1449
+ call_read_only=call_read_only,
1450
+ ),
1451
+ )
1452
+ return _apply_profile_resolution(
1453
+ Request(
1454
+ engine,
1455
+ mode,
1456
+ resolved.path,
1457
+ prompt,
1458
+ parts.argv,
1459
+ parts.model,
1460
+ model_alias=parts.model_alias,
1461
+ output_schema=output_schema,
1462
+ dry_run=dry_run,
1463
+ workspace_kind=resolved.kind,
1464
+ isolation_context=isolation_context,
1465
+ reasoning_effort=parts.reasoning_effort,
1466
+ reasoning_effort_source=parts.reasoning_effort_source,
1467
+ reasoning_capability_source=parts.reasoning_capability_source,
1468
+ reasoning_transport=parts.reasoning_transport,
1469
+ progress=progress,
1470
+ progress_initial_delay_sec=progress_initial_delay_sec,
1471
+ progress_interval_sec=progress_interval_sec,
1472
+ forbid_commit=forbid_commit,
1473
+ include_dirty=include_dirty,
1474
+ warnings=(*warnings, *parts.warnings),
1475
+ stdin_text=parts.stdin_text,
1476
+ prompt_file_text=parts.prompt_file_text,
1477
+ prompt_transport=parts.prompt_transport,
1478
+ display_argv=parts.display_argv,
1479
+ cleanup_workspace=cleanup_workspace,
1480
+ group=group,
1481
+ ),
1482
+ config,
1483
+ auth_profile_override=auth_profile_override,
1484
+ )
1485
+
1486
+
1487
+ def _dedupe_warnings(warnings: tuple[str, ...]) -> tuple[str, ...]:
1488
+ seen: set[str] = set()
1489
+ deduped: list[str] = []
1490
+ for warning in warnings:
1491
+ if warning in seen:
1492
+ continue
1493
+ seen.add(warning)
1494
+ deduped.append(warning)
1495
+ return tuple(deduped)
1496
+
1497
+
1498
+ def _apply_profile_resolution(
1499
+ request: Request, config: JsonObject, *, auth_profile_override: str | None = None
1500
+ ) -> Request:
1501
+ resolution = profiles.resolve_active_profile(
1502
+ config, os.environ, cli_override=auth_profile_override
1503
+ )
1504
+ env_overrides = dict(request.env_overrides or {})
1505
+ env_overrides.update(resolution.env)
1506
+ auth_profile = resolution.name
1507
+ fallback_profile = None
1508
+ if request.engine == "codex":
1509
+ if resolution.name is not None and resolution.codex_home is None:
1510
+ raise DelegateError(
1511
+ "profile_missing_codex_home",
1512
+ f"Profile {resolution.name!r} is active for a Codex Run but does not define CODEX_HOME.",
1513
+ )
1514
+ if resolution.name is not None:
1515
+ fallback_profile = profiles.codex_fallback_profile(config)
1516
+ return replace(
1517
+ request,
1518
+ warnings=_dedupe_warnings((*request.warnings, *resolution.warnings)),
1519
+ env_overrides=env_overrides or None,
1520
+ auth_profile=auth_profile,
1521
+ fallback_auth_profile=fallback_profile,
1522
+ profile_resolution=resolution,
1523
+ )
1524
+
1525
+
1526
+ def resolve_effective_reasoning_effort(
1527
+ config: JsonObject,
1528
+ engine: str,
1529
+ requested: str | None,
1530
+ *,
1531
+ reasoning_effort_source: str | None,
1532
+ ) -> tuple[str | None, str | None]:
1533
+ if requested is not None:
1534
+ try:
1535
+ return reasoning.normalize_effort(requested), reasoning_effort_source or "cli"
1536
+ except reasoning.ReasoningCapabilityError as exc:
1537
+ raise DelegateError(exc.error, exc.message) from exc
1538
+ section = config.get(engine)
1539
+ if isinstance(section, dict):
1540
+ default = section.get("defaultReasoningEffort")
1541
+ if default is not None:
1542
+ try:
1543
+ return reasoning.normalize_effort(default), "config"
1544
+ except reasoning.ReasoningCapabilityError as exc:
1545
+ raise DelegateError(exc.error, exc.message) from exc
1546
+ return None, None
1547
+
1548
+
1549
+ def _capability_with_config_fallback(
1550
+ resolver: Callable[[], reasoning.ReasoningCapability | None],
1551
+ *,
1552
+ engine: str,
1553
+ effort_source: str | None,
1554
+ ) -> tuple[reasoning.ReasoningCapability | None, tuple[str, ...]]:
1555
+ """Resolve a capability, degrading config-default failures to a warning.
1556
+
1557
+ An explicit per-run --reasoning-effort fails closed, but a config
1558
+ defaultReasoningEffort is a preference: when it cannot be satisfied (no
1559
+ resolved model, no capability declaration, no cursor mapping), the run
1560
+ proceeds without reasoning effort instead of bricking every launch of the
1561
+ engine until the config is edited.
1562
+ """
1563
+ try:
1564
+ return resolver(), ()
1565
+ except reasoning.ReasoningCapabilityError as exc:
1566
+ if effort_source != "config":
1567
+ raise DelegateError(exc.error, exc.message) from exc
1568
+ return None, (f"ignoring {engine}.defaultReasoningEffort: {exc.message}",)
1569
+ except DelegateError as exc:
1570
+ if effort_source != "config":
1571
+ raise
1572
+ return None, (f"ignoring {engine}.defaultReasoningEffort: {exc.message}",)
1573
+
1574
+
1575
+ def resolve_cursor_reasoning_capability(
1576
+ cursor_config: JsonObject,
1577
+ requested_effort: str | None,
1578
+ ) -> reasoning.ReasoningCapability | None:
1579
+ if requested_effort is None:
1580
+ return None
1581
+ default_model = cursor_config.get("defaultModel")
1582
+ alias = default_model if isinstance(default_model, str) and default_model else None
1583
+ mappings = cursor_config.get("reasoningEffortModels")
1584
+ if not isinstance(mappings, dict):
1585
+ raise DelegateError(
1586
+ "unsupported_reasoning_effort",
1587
+ reasoning.format_explicit_reasoning_effort_error(
1588
+ harness="cursor",
1589
+ alias=alias,
1590
+ model=alias,
1591
+ effort=requested_effort,
1592
+ detail="requires cursor.reasoningEffortModels",
1593
+ ),
1594
+ )
1595
+ model = mappings.get(requested_effort)
1596
+ if not isinstance(model, str) or not model:
1597
+ supported = sorted(
1598
+ effort
1599
+ for effort, mapped_model in mappings.items()
1600
+ if isinstance(effort, str) and effort and isinstance(mapped_model, str) and mapped_model
1601
+ )
1602
+ raise DelegateError(
1603
+ "unsupported_reasoning_effort",
1604
+ reasoning.format_explicit_reasoning_effort_error(
1605
+ harness="cursor",
1606
+ alias=alias,
1607
+ model=alias,
1608
+ effort=requested_effort,
1609
+ supported=supported or None,
1610
+ detail="requires a configured model mapping",
1611
+ ),
1612
+ )
1613
+ return reasoning.ReasoningCapability(
1614
+ harness="cursor",
1615
+ model=model,
1616
+ effort=requested_effort,
1617
+ supported_efforts=(requested_effort,),
1618
+ default_effort=None,
1619
+ transport=reasoning.TRANSPORT_BY_HARNESS["cursor"],
1620
+ source="config",
1621
+ )
1622
+
1623
+
1624
+ def reasoning_request_kwargs(
1625
+ capability: reasoning.ReasoningCapability | None,
1626
+ source: str | None,
1627
+ ) -> JsonObject:
1628
+ if capability is None:
1629
+ return {}
1630
+ return {
1631
+ "reasoning_effort": capability.effort,
1632
+ "reasoning_effort_source": source or "cli",
1633
+ "reasoning_capability_source": capability.source,
1634
+ "reasoning_transport": capability.transport,
1635
+ }
1636
+
1637
+
1638
+ def _native_reasoning_request_kwargs(
1639
+ effort: str | None,
1640
+ source: str | None,
1641
+ *,
1642
+ transport: str,
1643
+ capability_source: str,
1644
+ ) -> JsonObject:
1645
+ if effort is None:
1646
+ return {}
1647
+ return {
1648
+ "reasoning_effort": effort,
1649
+ "reasoning_effort_source": source,
1650
+ "reasoning_capability_source": capability_source,
1651
+ "reasoning_transport": transport,
1652
+ }
1653
+
1654
+
1655
+ def claude_reasoning_request_kwargs(effort: str | None, source: str | None) -> JsonObject:
1656
+ """Reasoning payload fields for Claude's static native-effort flag.
1657
+
1658
+ The sibling of ``reasoning_request_kwargs`` for the engine that resolves a
1659
+ flag (``--effort``) rather than a ``ReasoningCapability`` object. Returns
1660
+ ``{}`` when no effort applies so the request-parts defaults stand.
1661
+ """
1662
+ return _native_reasoning_request_kwargs(
1663
+ effort,
1664
+ source,
1665
+ transport=reasoning.TRANSPORT_CLAUDE_EFFORT_FLAG,
1666
+ capability_source="static",
1667
+ )
1668
+
1669
+
1670
+ def grok_reasoning_request_kwargs(effort: str | None, source: str | None) -> JsonObject:
1671
+ return _native_reasoning_request_kwargs(
1672
+ effort,
1673
+ source,
1674
+ transport=reasoning.TRANSPORT_GROK_EFFORT_FLAG,
1675
+ capability_source="static",
1676
+ )
1677
+
1678
+
1679
+ def _resolve_default_model(section: JsonObject) -> str | None:
1680
+ default_model = section.get("defaultModel")
1681
+ return default_model if isinstance(default_model, str) and default_model else None