self-evolve-framework 1.5.0 → 1.6.0

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 (149) hide show
  1. package/package.json +1 -1
  2. package/template/skills/skillopt-sleep/SKILL.md +42 -0
  3. package/template/skills/skillopt-sleep/configs/_base_/default.yaml +103 -0
  4. package/template/skills/skillopt-sleep/configs/alfworld/default.yaml +29 -0
  5. package/template/skills/skillopt-sleep/configs/docvqa/default.yaml +28 -0
  6. package/template/skills/skillopt-sleep/configs/features/soft_gate.yaml +47 -0
  7. package/template/skills/skillopt-sleep/configs/livemathematicianbench/default.yaml +22 -0
  8. package/template/skills/skillopt-sleep/configs/officeqa/default.yaml +34 -0
  9. package/template/skills/skillopt-sleep/configs/searchqa/default.yaml +32 -0
  10. package/template/skills/skillopt-sleep/configs/spreadsheetbench/default.yaml +34 -0
  11. package/template/skills/skillopt-sleep/scripts/framework/skillopt/__init__.py +28 -0
  12. package/template/skills/skillopt-sleep/scripts/framework/skillopt/config.py +282 -0
  13. package/template/skills/skillopt-sleep/scripts/framework/skillopt/datasets/__init__.py +7 -0
  14. package/template/skills/skillopt-sleep/scripts/framework/skillopt/datasets/base.py +512 -0
  15. package/template/skills/skillopt-sleep/scripts/framework/skillopt/engine/__init__.py +9 -0
  16. package/template/skills/skillopt-sleep/scripts/framework/skillopt/engine/trainer.py +2379 -0
  17. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/__init__.py +1 -0
  18. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/README.md +43 -0
  19. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/config_template.yaml +55 -0
  20. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/env_template.py +151 -0
  21. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/_template/loader_template.py +87 -0
  22. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/__init__.py +5 -0
  23. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/adapter.py +428 -0
  24. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/dataloader.py +123 -0
  25. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/analyst_error.md +55 -0
  26. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/analyst_success.md +33 -0
  27. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_no_history.md +8 -0
  28. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_with_history.md +9 -0
  29. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/prompts/rollout_with_memory.md +16 -0
  30. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/reflect.py +4 -0
  31. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/rollout.py +366 -0
  32. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/skills/initial.md +45 -0
  33. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/__init__.py +9 -0
  34. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_envs.py +221 -0
  35. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_projection.py +60 -0
  36. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/alfworld_prompts.py +8 -0
  37. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/config_tw.yaml +145 -0
  38. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/env_base.py +84 -0
  39. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/env_manager.py +139 -0
  40. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/alfworld/vendor/memory.py +87 -0
  41. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/base.py +329 -0
  42. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/__init__.py +1 -0
  43. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/adapter.py +90 -0
  44. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/dataloader.py +61 -0
  45. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/evaluator.py +113 -0
  46. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/analyst_error.md +35 -0
  47. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/analyst_success.md +24 -0
  48. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/prompts/rollout_system.md +12 -0
  49. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/rollout.py +391 -0
  50. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/docvqa/skills/initial.md +11 -0
  51. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/__init__.py +1 -0
  52. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/adapter.py +129 -0
  53. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/dataloader.py +308 -0
  54. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/evaluator.py +62 -0
  55. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/analyst_error.md +37 -0
  56. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/analyst_success.md +25 -0
  57. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/prompts/rollout_system.md +12 -0
  58. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/reflect.py +4 -0
  59. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/rollout.py +434 -0
  60. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/livemathematicianbench/skills/initial.md +16 -0
  61. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/__init__.py +1 -0
  62. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/adapter.py +112 -0
  63. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/dataloader.py +71 -0
  64. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/evaluator.py +46 -0
  65. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/analyst_error.md +37 -0
  66. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/analyst_success.md +25 -0
  67. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/prompts/rollout_system.md +15 -0
  68. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/rollout.py +799 -0
  69. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/skills/initial.md +15 -0
  70. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/officeqa/tool_runtime.py +552 -0
  71. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/__init__.py +1 -0
  72. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/adapter.py +96 -0
  73. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/dataloader.py +42 -0
  74. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/evaluator.py +100 -0
  75. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/analyst_error.md +46 -0
  76. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/analyst_success.md +32 -0
  77. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/prompts/rollout_system.md +13 -0
  78. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/reflect.py +4 -0
  79. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/rollout.py +494 -0
  80. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/searchqa/skills/initial.md +3 -0
  81. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/__init__.py +5 -0
  82. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/adapter.py +159 -0
  83. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/codegen_agent.py +731 -0
  84. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/dataloader.py +37 -0
  85. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/evaluator.py +158 -0
  86. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/executor.py +67 -0
  87. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/analyst_error.md +46 -0
  88. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/analyst_success.md +32 -0
  89. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/codegen_system.md +1 -0
  90. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/critical_rules.md +9 -0
  91. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/prompts/react_system.md +21 -0
  92. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/react_agent.py +395 -0
  93. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/reflect.py +4 -0
  94. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/rollout.py +979 -0
  95. package/template/skills/skillopt-sleep/scripts/framework/skillopt/envs/spreadsheetbench/skills/initial.md +56 -0
  96. package/template/skills/skillopt-sleep/scripts/framework/skillopt/evaluation/__init__.py +13 -0
  97. package/template/skills/skillopt-sleep/scripts/framework/skillopt/evaluation/gate.py +148 -0
  98. package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/__init__.py +15 -0
  99. package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/aggregate.py +253 -0
  100. package/template/skills/skillopt-sleep/scripts/framework/skillopt/gradient/reflect.py +635 -0
  101. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/__init__.py +514 -0
  102. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/azure_openai.py +915 -0
  103. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/backend_config.py +185 -0
  104. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/claude_backend.py +371 -0
  105. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/codex_backend.py +666 -0
  106. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/codex_harness.py +1057 -0
  107. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/common.py +229 -0
  108. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/minimax_backend.py +277 -0
  109. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/qwen_backend.py +456 -0
  110. package/template/skills/skillopt-sleep/scripts/framework/skillopt/model/router.py +236 -0
  111. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/__init__.py +15 -0
  112. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/appendix.py +156 -0
  113. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/clip.py +109 -0
  114. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/lr_autonomous.py +108 -0
  115. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/meta_skill.py +79 -0
  116. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/rewrite.py +59 -0
  117. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/scheduler.py +127 -0
  118. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/select.py +4 -0
  119. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/skill.py +201 -0
  120. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/skill_aware.py +206 -0
  121. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/slow_update.py +396 -0
  122. package/template/skills/skillopt-sleep/scripts/framework/skillopt/optimizer/update_modes.py +135 -0
  123. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/__init__.py +63 -0
  124. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error.md +41 -0
  125. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error_full_rewrite.md +32 -0
  126. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_error_rewrite.md +44 -0
  127. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success.md +36 -0
  128. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success_full_rewrite.md +30 -0
  129. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/analyst_success_rewrite.md +33 -0
  130. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/lr_autonomous.md +20 -0
  131. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure.md +30 -0
  132. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure_full_rewrite.md +28 -0
  133. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_failure_rewrite.md +26 -0
  134. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final.md +33 -0
  135. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final_full_rewrite.md +28 -0
  136. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_final_rewrite.md +25 -0
  137. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success.md +28 -0
  138. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success_full_rewrite.md +28 -0
  139. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/merge_success_rewrite.md +25 -0
  140. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/meta_skill.md +40 -0
  141. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/ranking.md +20 -0
  142. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/ranking_rewrite.md +15 -0
  143. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/rewrite_skill.md +25 -0
  144. package/template/skills/skillopt-sleep/scripts/framework/skillopt/prompts/slow_update.md +60 -0
  145. package/template/skills/skillopt-sleep/scripts/framework/skillopt/scheduler/__init__.py +8 -0
  146. package/template/skills/skillopt-sleep/scripts/framework/skillopt/types.py +306 -0
  147. package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/__init__.py +4 -0
  148. package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/json_utils.py +172 -0
  149. package/template/skills/skillopt-sleep/scripts/framework/skillopt/utils/scoring.py +28 -0
@@ -0,0 +1,1057 @@
1
+ """Helpers for running exec backends as the target harness."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import json
6
+ import os
7
+ import re
8
+ import shutil
9
+ import subprocess
10
+ import threading
11
+ import traceback
12
+ from typing import Any
13
+
14
+ from skillopt.model.backend_config import (
15
+ get_claude_code_exec_config,
16
+ get_codex_exec_config,
17
+ get_target_backend,
18
+ )
19
+
20
+
21
+ ANSWER_SCHEMA: dict[str, Any] = {
22
+ "type": "object",
23
+ "properties": {
24
+ "final_response": {
25
+ "type": "string",
26
+ "description": "The exact final answer text to return, preserving required <answer>...</answer> tags.",
27
+ },
28
+ "final_answer": {
29
+ "type": "string",
30
+ "description": "The concise answer value without explanation, if separable.",
31
+ },
32
+ },
33
+ "required": ["final_response", "final_answer"],
34
+ "additionalProperties": False,
35
+ }
36
+
37
+
38
+ def render_skill_md(
39
+ skill_content: str,
40
+ *,
41
+ name: str = "skillopt-target",
42
+ description: str = "Dynamic ReflACT skill for the current benchmark task.",
43
+ preamble: str = "",
44
+ ) -> str:
45
+ body = skill_content.strip() or "No additional dynamic guidance was provided for this task."
46
+ chunks = [
47
+ "---",
48
+ f'name: "{name}"',
49
+ f'description: "{description}"',
50
+ "---",
51
+ "",
52
+ "# ReflACT Target Skill",
53
+ "",
54
+ ]
55
+ if preamble.strip():
56
+ chunks.append(preamble.strip())
57
+ chunks.append("")
58
+ chunks.extend([
59
+ "## Dynamic Guidance",
60
+ "",
61
+ body,
62
+ "",
63
+ ])
64
+ return "\n".join(chunks)
65
+
66
+
67
+ def prepare_workspace(
68
+ *,
69
+ work_dir: str,
70
+ skill_md: str,
71
+ task_text: str = "",
72
+ task_filename: str = "task.md",
73
+ images: list[str] | None = None,
74
+ extra_files: dict[str, str] | None = None,
75
+ copy_files: list[tuple[str, str]] | None = None,
76
+ link_dirs: list[tuple[str, str]] | None = None,
77
+ ) -> tuple[str, str]:
78
+ if os.path.exists(work_dir):
79
+ shutil.rmtree(work_dir)
80
+ os.makedirs(os.path.join(work_dir, ".agents", "skills", "skillopt-target"), exist_ok=True)
81
+
82
+ skill_path = os.path.join(work_dir, ".agents", "skills", "skillopt-target", "SKILL.md")
83
+ with open(skill_path, "w", encoding="utf-8") as f:
84
+ f.write(skill_md)
85
+
86
+ task_path = os.path.join(work_dir, task_filename)
87
+ if task_text:
88
+ with open(task_path, "w", encoding="utf-8") as f:
89
+ f.write(task_text)
90
+
91
+ if extra_files:
92
+ for rel_path, content in extra_files.items():
93
+ full_path = os.path.join(work_dir, rel_path)
94
+ parent = os.path.dirname(full_path)
95
+ if parent:
96
+ os.makedirs(parent, exist_ok=True)
97
+ with open(full_path, "w", encoding="utf-8") as f:
98
+ f.write(content)
99
+
100
+ if copy_files:
101
+ for src, rel_dst in copy_files:
102
+ dst = os.path.join(work_dir, rel_dst)
103
+ parent = os.path.dirname(dst)
104
+ if parent:
105
+ os.makedirs(parent, exist_ok=True)
106
+ shutil.copy2(src, dst)
107
+
108
+ if link_dirs:
109
+ for src, rel_dst in link_dirs:
110
+ dst = os.path.join(work_dir, rel_dst)
111
+ parent = os.path.dirname(dst)
112
+ if parent:
113
+ os.makedirs(parent, exist_ok=True)
114
+ os.symlink(os.path.abspath(src), dst)
115
+
116
+ attachment_lines: list[str] = []
117
+ if images:
118
+ attachments_dir = os.path.join(work_dir, "attachments")
119
+ os.makedirs(attachments_dir, exist_ok=True)
120
+ for index, image in enumerate(images, 1):
121
+ if not os.path.exists(image):
122
+ raise FileNotFoundError(image)
123
+ src = os.path.abspath(image)
124
+ base = os.path.basename(src) or f"image_{index}"
125
+ dst_name = f"{index:02d}_{base}"
126
+ dst = os.path.join(attachments_dir, dst_name)
127
+ if os.path.abspath(src) != os.path.abspath(dst):
128
+ shutil.copy2(src, dst)
129
+ rel_dst = os.path.relpath(dst, work_dir)
130
+ attachment_lines.append(f"- `{rel_dst}` (source: `{src}`)")
131
+
132
+ if attachment_lines:
133
+ with open(os.path.join(work_dir, "ATTACHMENTS.md"), "w", encoding="utf-8") as f:
134
+ f.write(
135
+ "# Attachments\n\n"
136
+ "Use these local files when the task refers to attached images or documents.\n\n"
137
+ + "\n".join(attachment_lines)
138
+ + "\n"
139
+ )
140
+
141
+ return skill_path, task_path
142
+
143
+
144
+ def _build_codex_trace_summary(raw: str, response: str) -> str:
145
+ lines = [ln.rstrip() for ln in (raw or "").splitlines()]
146
+
147
+ def _find(prefix: str) -> str:
148
+ for ln in lines:
149
+ if ln.startswith(prefix):
150
+ return ln[len(prefix):].strip()
151
+ return ""
152
+
153
+ sandbox = _find("sandbox: ")
154
+ reasoning = _find("reasoning effort: ")
155
+ task_read = "unknown"
156
+ skill_read = "unknown"
157
+ exec_errors: list[str] = []
158
+ tokens_used = ""
159
+
160
+ for idx, ln in enumerate(lines):
161
+ if ln.startswith("exec"):
162
+ cmd = lines[idx + 1] if idx + 1 < len(lines) else ""
163
+ outcome = lines[idx + 2] if idx + 2 < len(lines) else ""
164
+ joined = f"{cmd}\n{outcome}"
165
+ if "task.md" in joined:
166
+ if "succeeded" in outcome:
167
+ task_read = "success"
168
+ elif "failed" in outcome or "ERROR" in outcome:
169
+ task_read = "failed"
170
+ if "SKILL.md" in joined:
171
+ if "succeeded" in outcome:
172
+ skill_read = "success"
173
+ elif "failed" in outcome or "ERROR" in outcome:
174
+ skill_read = "failed"
175
+ if ln.startswith("ERROR:"):
176
+ exec_errors.append(ln[len("ERROR:"):].strip())
177
+ if ln == "tokens used" and idx + 1 < len(lines):
178
+ tokens_used = lines[idx + 1].strip()
179
+
180
+ match = re.search(r"<answer>\s*([A-E])\s*</answer>", response or "", re.IGNORECASE)
181
+ if match:
182
+ answer_format = "well_formed"
183
+ answer_label = match.group(1).upper()
184
+ elif "<answer>" in (response or "").lower():
185
+ answer_format = "tagged_nonlabel"
186
+ answer_label = ""
187
+ elif (response or "").strip():
188
+ answer_format = "plain_text"
189
+ answer_label = ""
190
+ else:
191
+ answer_format = "missing"
192
+ answer_label = ""
193
+
194
+ parts = ["Codex Trace Summary"]
195
+ if sandbox:
196
+ parts.append(f"- sandbox: {sandbox}")
197
+ if reasoning:
198
+ parts.append(f"- reasoning: {reasoning}")
199
+ parts.append(f"- read task.md: {task_read}")
200
+ parts.append(f"- read SKILL.md: {skill_read}")
201
+ if exec_errors:
202
+ parts.append(f"- shell/tool errors: {' | '.join(exec_errors[:3])}")
203
+ else:
204
+ parts.append("- shell/tool errors: none")
205
+ parts.append(f"- final answer format: {answer_format}")
206
+ parts.append(f"- final answer label: {answer_label or '(none)'}")
207
+ if tokens_used:
208
+ parts.append(f"- tokens used: {tokens_used}")
209
+ return "\n".join(parts)
210
+
211
+
212
+ def _build_claude_trace_summary(raw: str, response: str) -> str:
213
+ answer_format = "missing"
214
+ if "<answer>" in (response or "").lower():
215
+ answer_format = "tagged"
216
+ elif (response or "").strip():
217
+ answer_format = "plain_text"
218
+ errors: list[str] = []
219
+ for ln in (raw or "").splitlines():
220
+ if "error" in ln.lower() or "traceback" in ln.lower():
221
+ errors.append(ln.strip())
222
+ if len(errors) >= 3:
223
+ break
224
+ parts = ["Claude Code Trace Summary", f"- final answer format: {answer_format}"]
225
+ parts.append(f"- final response chars: {len(response or '')}")
226
+ parts.append(f"- errors: {' | '.join(errors) if errors else 'none'}")
227
+ return "\n".join(parts)
228
+
229
+
230
+ def _persist_artifacts(
231
+ *,
232
+ work_dir: str,
233
+ raw: str,
234
+ response: str,
235
+ prefix: str,
236
+ summary_builder,
237
+ ) -> None:
238
+ pred_dir = os.path.dirname(work_dir.rstrip(os.sep))
239
+ raw_path = os.path.join(pred_dir, f"{prefix}_raw.txt")
240
+ summary_path = os.path.join(pred_dir, f"{prefix}_trace_summary.txt")
241
+
242
+ combined_raw = raw
243
+ if os.path.exists(raw_path):
244
+ with open(raw_path, encoding="utf-8") as f:
245
+ prev = f.read()
246
+ combined_raw = f"{prev}\n\n===== TURN BREAK =====\n\n{raw}" if prev.strip() else raw
247
+
248
+ with open(raw_path, "w", encoding="utf-8") as f:
249
+ f.write(combined_raw)
250
+ with open(summary_path, "w", encoding="utf-8") as f:
251
+ f.write(summary_builder(combined_raw, response))
252
+
253
+
254
+ def _persist_codex_artifacts(work_dir: str, raw: str, response: str) -> None:
255
+ _persist_artifacts(
256
+ work_dir=work_dir,
257
+ raw=raw,
258
+ response=response,
259
+ prefix="codex",
260
+ summary_builder=_build_codex_trace_summary,
261
+ )
262
+
263
+
264
+ def _persist_claude_artifacts(work_dir: str, raw: str, response: str) -> None:
265
+ _persist_artifacts(
266
+ work_dir=work_dir,
267
+ raw=raw,
268
+ response=response,
269
+ prefix="claude",
270
+ summary_builder=_build_claude_trace_summary,
271
+ )
272
+
273
+
274
+ def parse_codex_raw(raw: str) -> dict:
275
+ """Parse raw Codex CLI output into step sections.
276
+
277
+ Returns a dict with:
278
+ - ``steps``: ordered sections beginning at the first ``user/codex/exec`` marker
279
+ - ``trace_body``: raw trace starting at the first marker
280
+ """
281
+ lines = (raw or "").splitlines()
282
+ markers = {"user", "codex", "exec"}
283
+ first_step_line: int | None = None
284
+ for idx, line in enumerate(lines):
285
+ if line in markers:
286
+ first_step_line = idx
287
+ break
288
+ if first_step_line is None:
289
+ return {"steps": [], "trace_body": ""}
290
+
291
+ steps: list[dict] = []
292
+ current: dict | None = None
293
+ for idx in range(first_step_line, len(lines)):
294
+ line = lines[idx]
295
+ if line in markers:
296
+ if current is not None:
297
+ current["end_line"] = idx
298
+ current["content"] = "\n".join(current["content_lines"]).strip()
299
+ current.pop("content_lines", None)
300
+ steps.append(current)
301
+ current = {
302
+ "index": len(steps) + 1,
303
+ "type": line,
304
+ "start_line": idx,
305
+ "content_lines": [],
306
+ }
307
+ continue
308
+ if current is not None:
309
+ current["content_lines"].append(line)
310
+ if current is not None:
311
+ current["end_line"] = len(lines)
312
+ current["content"] = "\n".join(current["content_lines"]).strip()
313
+ current.pop("content_lines", None)
314
+ steps.append(current)
315
+
316
+ trace_body = "\n".join(lines[first_step_line:]).strip()
317
+ return {"steps": steps, "trace_body": trace_body}
318
+
319
+
320
+ def format_codex_trace_steps(raw: str, *, max_chars: int = 4000) -> str:
321
+ """Render parsed Codex trace into numbered compact steps for optimizer prompts."""
322
+ parsed = parse_codex_raw(raw)
323
+ steps = parsed["steps"]
324
+ if not steps:
325
+ return ""
326
+
327
+ rendered: list[str] = []
328
+ for step in steps:
329
+ summary = ""
330
+ content = str(step.get("content") or "").strip()
331
+ if step["type"] == "exec":
332
+ body_lines = [ln.strip() for ln in content.splitlines() if ln.strip()]
333
+ cmd = body_lines[0] if body_lines else ""
334
+ status = ""
335
+ for ln in body_lines[1:]:
336
+ low = ln.lower()
337
+ if "succeeded in" in low or "failed in" in low or "timed out" in low or low.startswith("error"):
338
+ status = ln
339
+ break
340
+ summary = cmd
341
+ if status:
342
+ summary = f"{summary} | {status}" if summary else status
343
+ else:
344
+ summary = " ".join(content.splitlines())
345
+ summary = summary[:500] if summary else "(empty)"
346
+ rendered.append(f"[{step['index']}] {step['type']}: {summary}")
347
+
348
+ text = "\n".join(rendered)
349
+ if len(text) > max_chars:
350
+ text = text[:max_chars] + "\n...[trace steps truncated]..."
351
+ return text
352
+
353
+
354
+ def extract_codex_trace_prefix(raw: str, *, after_step: int) -> str:
355
+ """Return raw trace body up to and including ``after_step``.
356
+
357
+ ``after_step <= 0`` yields an empty string.
358
+ """
359
+ if after_step <= 0:
360
+ return ""
361
+ parsed = parse_codex_raw(raw)
362
+ steps = parsed["steps"]
363
+ if not steps:
364
+ return ""
365
+ clamped = min(after_step, len(steps))
366
+ lines = parsed["trace_body"].splitlines()
367
+ end_line = int(steps[clamped - 1]["end_line"]) - int(steps[0]["start_line"])
368
+ return "\n".join(lines[:end_line]).strip()
369
+
370
+
371
+ _DENIED_DATA_DIR_NAMES = {"officeqa_split", "sealqa_split"}
372
+
373
+
374
+ def _normalize_tools(allowed_tools: list[str] | str | None) -> str:
375
+ if allowed_tools is None:
376
+ return ""
377
+ if isinstance(allowed_tools, str):
378
+ return ",".join(part.strip() for part in allowed_tools.split(",") if part.strip())
379
+ return ",".join(str(tool).strip() for tool in allowed_tools if str(tool).strip())
380
+
381
+
382
+ def _tools_list(allowed_tools: list[str] | str | None) -> list[str]:
383
+ tools = _normalize_tools(allowed_tools)
384
+ return [part.strip() for part in tools.split(",") if part.strip()]
385
+
386
+
387
+ def _validate_exec_path(path: str) -> str:
388
+ resolved = os.path.realpath(os.path.abspath(path))
389
+ parts = set(resolved.split(os.sep))
390
+ denied = parts & _DENIED_DATA_DIR_NAMES
391
+ if denied:
392
+ raise ValueError(f"Refusing to expose denied data directory to exec backend: {', '.join(sorted(denied))}")
393
+ return resolved
394
+
395
+
396
+ def _validated_add_dirs(work_dir: str, data_dirs: list[str] | None, images: list[str] | None) -> list[str]:
397
+ add_dirs = [_validate_exec_path(work_dir)]
398
+ for data_dir in data_dirs or []:
399
+ add_dirs.append(_validate_exec_path(data_dir))
400
+ for image in images or []:
401
+ add_dirs.append(_validate_exec_path(os.path.dirname(image) or work_dir))
402
+ deduped: list[str] = []
403
+ for path in add_dirs:
404
+ if path not in deduped:
405
+ deduped.append(path)
406
+ return deduped
407
+
408
+
409
+ def _sdk_mode(value: Any) -> str:
410
+ mode = str(value or "auto").strip().lower()
411
+ if mode in {"1", "true", "yes", "on", "sdk"}:
412
+ return "sdk"
413
+ if mode in {"0", "false", "no", "off", "cli"}:
414
+ return "cli"
415
+ return "auto"
416
+
417
+
418
+ def _claude_effort(value: Any) -> str:
419
+ effort = str(value or "medium").strip().lower()
420
+ if effort in {"", "none", "off"}:
421
+ return ""
422
+ if effort == "xhigh":
423
+ return "max"
424
+ if effort not in {"low", "medium", "high", "max"}:
425
+ return "medium"
426
+ return effort
427
+
428
+
429
+ def _json_default(obj: Any) -> Any:
430
+ if isinstance(obj, (str, int, float, bool)) or obj is None:
431
+ return obj
432
+ if isinstance(obj, (list, tuple)):
433
+ return list(obj)
434
+ if isinstance(obj, dict):
435
+ return obj
436
+ if hasattr(obj, "model_dump"):
437
+ return obj.model_dump(mode="json")
438
+ if hasattr(obj, "__dict__"):
439
+ return {k: v for k, v in vars(obj).items() if not k.startswith("_")}
440
+ return str(obj)
441
+
442
+
443
+ def _json_dumps(data: Any) -> str:
444
+ return json.dumps(data, ensure_ascii=False, indent=2, default=_json_default)
445
+
446
+
447
+ def _run_async(coro):
448
+ try:
449
+ asyncio.get_running_loop()
450
+ except RuntimeError:
451
+ return asyncio.run(coro)
452
+
453
+ box: dict[str, Any] = {}
454
+
455
+ def _target() -> None:
456
+ try:
457
+ box["result"] = asyncio.run(coro)
458
+ except BaseException as exc: # noqa: BLE001
459
+ box["exception"] = exc
460
+
461
+ thread = threading.Thread(target=_target, daemon=True)
462
+ thread.start()
463
+ thread.join()
464
+ if "exception" in box:
465
+ raise box["exception"]
466
+ return box.get("result")
467
+
468
+
469
+ def _exec_prompt(prompt: str, *, allow_file_edits: bool = False) -> str:
470
+ edit_instruction = (
471
+ "You may modify files in the workspace when the task asks you to create an artifact. "
472
+ if allow_file_edits
473
+ else "Do not modify files. "
474
+ )
475
+ return (
476
+ "Use the workspace files to solve the task. Read task.md and the skill at "
477
+ ".agents/skills/skillopt-target/SKILL.md before answering. "
478
+ "If ATTACHMENTS.md exists, read it and inspect the listed local files. "
479
+ "Do not call a Skill tool; the ReflACT guidance is a local markdown file. "
480
+ f"Do not ask for permission. {edit_instruction}"
481
+ "Return only the final answer text, keeping any required <answer>...</answer> tags exactly.\n\n"
482
+ f"{_normalize_target_exec_prompt(prompt)}"
483
+ )
484
+
485
+
486
+ def _retry_prompt(prompt: str, attempt: int) -> str:
487
+ if attempt <= 0:
488
+ return prompt
489
+ return (
490
+ f"{prompt}\n\n"
491
+ "Previous execution returned an empty final response. Re-read task.md and "
492
+ ".agents/skills/skillopt-target/SKILL.md. If ATTACHMENTS.md exists, use the listed files. "
493
+ "Then produce the final answer inside <answer>...</answer>."
494
+ )
495
+
496
+
497
+ def _normalize_target_exec_prompt(prompt: str) -> str:
498
+ """Avoid wording that makes Claude Code call an unregistered Skill tool."""
499
+ text = prompt or ""
500
+ replacements = {
501
+ "Use the `skillopt-target` skill available in this workspace.": (
502
+ "Read `.agents/skills/skillopt-target/SKILL.md` directly; do not call a Skill tool."
503
+ ),
504
+ "- Use the local `skillopt-target` skill before writing code.": (
505
+ "- Read `.agents/skills/skillopt-target/SKILL.md` before writing code; do not call a Skill tool."
506
+ ),
507
+ }
508
+ for old, new in replacements.items():
509
+ text = text.replace(old, new)
510
+ return text
511
+
512
+
513
+ def _strict_schema(schema: dict[str, Any]) -> dict[str, Any]:
514
+ strict = json.loads(json.dumps(schema))
515
+ strict["additionalProperties"] = False
516
+ properties = strict.get("properties") or {}
517
+ strict["required"] = list(properties.keys())
518
+ return strict
519
+
520
+
521
+ def _structured_response(data: Any) -> tuple[str, str]:
522
+ if not isinstance(data, dict):
523
+ return "", f"Structured output was not an object: {type(data).__name__}"
524
+ final_response = str(data.get("final_response") or "").strip()
525
+ final_answer = str(data.get("final_answer") or "").strip()
526
+ if final_response:
527
+ return final_response, ""
528
+ if final_answer:
529
+ if "<answer>" in final_answer.lower():
530
+ return final_answer, ""
531
+ return f"<answer>{final_answer}</answer>", ""
532
+ return "", "Structured output did not contain a final response."
533
+
534
+
535
+ def _extract_claude_structured_output(messages: list[Any]) -> Any:
536
+ """Claude Code SDK can finish with error_during_execution after StructuredOutput."""
537
+ for msg in reversed(messages):
538
+ structured = getattr(msg, "structured_output", None)
539
+ if isinstance(structured, dict):
540
+ return structured
541
+
542
+ content = getattr(msg, "content", None)
543
+ if content is None and isinstance(msg, dict):
544
+ content = msg.get("content")
545
+ if not isinstance(content, list):
546
+ continue
547
+
548
+ for item in reversed(content):
549
+ name = getattr(item, "name", None)
550
+ payload = getattr(item, "input", None)
551
+ if isinstance(item, dict):
552
+ name = item.get("name", name)
553
+ payload = item.get("input", payload)
554
+ if name == "StructuredOutput" and isinstance(payload, dict):
555
+ return payload
556
+ return None
557
+
558
+
559
+ def _raw_exception(label: str, exc: BaseException) -> str:
560
+ return _json_dumps({
561
+ "backend": label,
562
+ "is_error": True,
563
+ "error_type": type(exc).__name__,
564
+ "error": str(exc),
565
+ "traceback": traceback.format_exc(),
566
+ })
567
+
568
+
569
+ def _run_claude_code_sdk_exec(
570
+ *,
571
+ work_dir: str,
572
+ prompt: str,
573
+ model: str,
574
+ timeout: int,
575
+ images: list[str] | None = None,
576
+ data_dirs: list[str] | None = None,
577
+ allowed_tools: list[str] | str | None = None,
578
+ permission_mode: str | None = None,
579
+ allow_file_edits: bool = False,
580
+ ) -> tuple[str, str]:
581
+ from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
582
+
583
+ async def _query() -> tuple[str, str]:
584
+ system_prompt: dict[str, Any] = {
585
+ "type": "preset",
586
+ "preset": "claude_code",
587
+ "append": (
588
+ "Use the workspace files to solve the task. Read task.md and the skill at "
589
+ ".agents/skills/skillopt-target/SKILL.md before answering. "
590
+ "If ATTACHMENTS.md exists, read it and inspect the listed local files. "
591
+ "Do not call a Skill tool; the ReflACT guidance is a local markdown file. "
592
+ + (
593
+ "You may modify files in the workspace when the task asks you to create an artifact. "
594
+ if allow_file_edits
595
+ else "Do not modify files. "
596
+ )
597
+ + "Return structured output whose final_response preserves required <answer>...</answer> tags."
598
+ ),
599
+ }
600
+ kwargs: dict[str, Any] = {
601
+ "system_prompt": system_prompt,
602
+ "output_format": {"type": "json_schema", "schema": ANSWER_SCHEMA},
603
+ "allowed_tools": _tools_list(allowed_tools) or ["Read", "Bash"],
604
+ "cwd": str(work_dir),
605
+ "permission_mode": permission_mode or "bypassPermissions",
606
+ "add_dirs": _validated_add_dirs(work_dir, data_dirs, images),
607
+ "max_buffer_size": 8 * 1024 * 1024,
608
+ }
609
+ config = get_claude_code_exec_config()
610
+ effort = _claude_effort(config.get("effort"))
611
+ if effort:
612
+ kwargs["effort"] = effort
613
+ max_thinking_tokens = int(config.get("max_thinking_tokens", 0) or 0)
614
+ if max_thinking_tokens > 0:
615
+ kwargs["max_thinking_tokens"] = max_thinking_tokens
616
+ options = ClaudeAgentOptions(**kwargs)
617
+ if model:
618
+ options.model = model.split("/", 1)[1] if model.startswith("anthropic/") else model
619
+
620
+ messages = []
621
+ async with ClaudeSDKClient(options) as client:
622
+ await client.query(_normalize_target_exec_prompt(prompt))
623
+ messages = [msg async for msg in client.receive_response()]
624
+ last = messages[-1] if messages else None
625
+ raw_structured_output = _extract_claude_structured_output(messages)
626
+ response, parse_error = _structured_response(raw_structured_output)
627
+ first = messages[0] if messages else None
628
+ first_data = getattr(first, "data", {}) if first is not None else {}
629
+ terminal_is_error = bool(getattr(last, "is_error", False)) if last is not None else False
630
+ raw = _json_dumps({
631
+ "backend": "claude_code_sdk",
632
+ "uuid": first_data.get("uuid", "") if isinstance(first_data, dict) else "",
633
+ "session_id": getattr(last, "session_id", "") if last is not None else "",
634
+ "model": first_data.get("model", model) if isinstance(first_data, dict) else model,
635
+ "tools": first_data.get("tools", _tools_list(allowed_tools)) if isinstance(first_data, dict) else _tools_list(allowed_tools),
636
+ "duration_ms": getattr(last, "duration_ms", 0) if last is not None else 0,
637
+ "total_cost_usd": getattr(last, "total_cost_usd", 0.0) if last is not None else 0.0,
638
+ "num_turns": getattr(last, "num_turns", 0) if last is not None else 0,
639
+ "usage": getattr(last, "usage", {}) if last is not None else {},
640
+ "result": getattr(last, "result", "") if last is not None else "",
641
+ "is_error": bool(parse_error) or (terminal_is_error and not response.strip()),
642
+ "terminal_is_error": terminal_is_error,
643
+ "parse_error": parse_error,
644
+ "raw_structured_output": raw_structured_output,
645
+ "messages": messages,
646
+ })
647
+ return response, raw
648
+
649
+ return _run_async(asyncio.wait_for(_query(), timeout=timeout))
650
+
651
+
652
+ def _run_claude_code_cli_exec(
653
+ *,
654
+ work_dir: str,
655
+ prompt: str,
656
+ model: str,
657
+ timeout: int,
658
+ images: list[str] | None = None,
659
+ data_dirs: list[str] | None = None,
660
+ allowed_tools: list[str] | str | None = None,
661
+ permission_mode: str | None = None,
662
+ allow_file_edits: bool = False,
663
+ ) -> tuple[str, str]:
664
+ config = get_claude_code_exec_config()
665
+ tools = "Read,Bash" if allowed_tools is None else _normalize_tools(allowed_tools)
666
+ cmd = [
667
+ str(config["path"]),
668
+ "-p",
669
+ "--output-format",
670
+ "text",
671
+ "--permission-mode",
672
+ permission_mode or "bypassPermissions",
673
+ "--add-dir",
674
+ work_dir,
675
+ "--tools",
676
+ tools,
677
+ "--allowedTools",
678
+ tools,
679
+ ]
680
+ if config.get("profile"):
681
+ cmd.extend(["--settings", '{"env":{"CLAUDE_CODE_USE_BEDROCK":"0"}}'])
682
+ cmd.extend(["--append-system-prompt", f"Profile: {config['profile']}"])
683
+ if model:
684
+ cmd.extend(["--model", model])
685
+ effort = _claude_effort(config.get("effort"))
686
+ if effort:
687
+ cmd.extend(["--effort", effort])
688
+ max_thinking_tokens = int(config.get("max_thinking_tokens", 0) or 0)
689
+ if max_thinking_tokens > 0:
690
+ cmd.extend(["--max-thinking-tokens", str(max_thinking_tokens)])
691
+ for data_dir in data_dirs or []:
692
+ cmd.extend(["--add-dir", _validate_exec_path(data_dir)])
693
+ if images:
694
+ for image in images:
695
+ cmd.extend(["--add-dir", _validate_exec_path(os.path.dirname(image) or work_dir)])
696
+ cmd.extend(["--", _exec_prompt(prompt, allow_file_edits=allow_file_edits)])
697
+
698
+ try:
699
+ proc = subprocess.run(
700
+ cmd,
701
+ cwd=work_dir,
702
+ capture_output=True,
703
+ text=True,
704
+ timeout=timeout,
705
+ )
706
+ except subprocess.TimeoutExpired as exc:
707
+ stdout = exc.stdout or ""
708
+ stderr = exc.stderr or ""
709
+ raw = stdout
710
+ if stderr:
711
+ raw = f"{raw}\n[stderr]\n{stderr}" if raw else stderr
712
+ return "", raw
713
+
714
+ stdout = proc.stdout or ""
715
+ stderr = proc.stderr or ""
716
+ raw = stdout
717
+ if stderr:
718
+ raw = f"{raw}\n[stderr]\n{stderr}" if raw else stderr
719
+ response = stdout.strip()
720
+ if proc.returncode != 0 and not response:
721
+ return "", raw
722
+ return response, raw
723
+
724
+
725
+ def run_claude_code_exec(
726
+ *,
727
+ work_dir: str,
728
+ prompt: str,
729
+ model: str,
730
+ timeout: int,
731
+ images: list[str] | None = None,
732
+ data_dirs: list[str] | None = None,
733
+ allowed_tools: list[str] | str | None = None,
734
+ permission_mode: str | None = None,
735
+ allow_file_edits: bool = False,
736
+ ) -> tuple[str, str]:
737
+ config = get_claude_code_exec_config()
738
+ mode = _sdk_mode(config.get("use_sdk"))
739
+ retries = int(config.get("empty_response_retries", 0) or 0)
740
+ last_response = ""
741
+ all_raw: list[str] = []
742
+
743
+ for attempt in range(retries + 1):
744
+ attempt_prompt = _retry_prompt(prompt, attempt)
745
+ if mode != "cli":
746
+ try:
747
+ response, raw = _run_claude_code_sdk_exec(
748
+ work_dir=work_dir,
749
+ prompt=attempt_prompt,
750
+ model=model,
751
+ timeout=timeout,
752
+ images=images,
753
+ data_dirs=data_dirs,
754
+ allowed_tools=allowed_tools,
755
+ permission_mode=permission_mode,
756
+ allow_file_edits=allow_file_edits,
757
+ )
758
+ all_raw.append(f"===== CLAUDE SDK ATTEMPT {attempt + 1} =====\n{raw}")
759
+ if response.strip():
760
+ combined = "\n\n".join(all_raw)
761
+ _persist_claude_artifacts(work_dir, combined, response)
762
+ return response, combined
763
+ except (ImportError, ModuleNotFoundError) as exc:
764
+ raw = _raw_exception("claude_code_sdk", exc)
765
+ all_raw.append(f"===== CLAUDE SDK ATTEMPT {attempt + 1} =====\n{raw}")
766
+ if mode == "sdk":
767
+ _persist_claude_artifacts(work_dir, "\n\n".join(all_raw), "")
768
+ raise
769
+ except Exception as exc: # noqa: BLE001
770
+ raw = _raw_exception("claude_code_sdk", exc)
771
+ all_raw.append(f"===== CLAUDE SDK ATTEMPT {attempt + 1} =====\n{raw}")
772
+ if mode == "sdk" and attempt >= retries:
773
+ _persist_claude_artifacts(work_dir, "\n\n".join(all_raw), "")
774
+ raise
775
+ if mode != "sdk":
776
+ response, raw = _run_claude_code_cli_exec(
777
+ work_dir=work_dir,
778
+ prompt=attempt_prompt,
779
+ model=model,
780
+ timeout=timeout,
781
+ images=images,
782
+ data_dirs=data_dirs,
783
+ allowed_tools=allowed_tools,
784
+ permission_mode=permission_mode,
785
+ allow_file_edits=allow_file_edits,
786
+ )
787
+ all_raw.append(f"===== CLAUDE CLI ATTEMPT {attempt + 1} =====\n{raw}")
788
+ last_response = response
789
+ if response.strip():
790
+ combined = "\n\n".join(all_raw)
791
+ _persist_claude_artifacts(work_dir, combined, response)
792
+ return response, combined
793
+
794
+ combined = "\n\n".join(all_raw)
795
+ _persist_claude_artifacts(work_dir, combined, last_response)
796
+ return last_response, combined
797
+
798
+
799
+ def _run_codex_sdk_exec(
800
+ *,
801
+ work_dir: str,
802
+ prompt: str,
803
+ model: str,
804
+ timeout: int,
805
+ images: list[str] | None = None,
806
+ data_dirs: list[str] | None = None,
807
+ ) -> tuple[str, str]:
808
+ from openai_codex_sdk import Codex
809
+
810
+ for data_dir in data_dirs or []:
811
+ _validate_exec_path(data_dir)
812
+ for image in images or []:
813
+ _validate_exec_path(os.path.dirname(image) or work_dir)
814
+
815
+ async def _query() -> tuple[str, str]:
816
+ config = get_codex_exec_config()
817
+ reasoning_effort = str(config.get("reasoning_effort", "") or "").strip()
818
+ thread_options: dict[str, Any] = {
819
+ "working_directory": work_dir,
820
+ "skip_git_repo_check": True,
821
+ "sandbox_mode": str(config.get("sandbox") or "workspace-write"),
822
+ "network_access_enabled": bool(config.get("network_access", False)),
823
+ "web_search_enabled": bool(config.get("web_search", False)),
824
+ "approval_policy": str(config.get("approval_policy") or "never"),
825
+ }
826
+ if model:
827
+ thread_options["model"] = model
828
+ if data_dirs:
829
+ thread_options["additional_directories"] = data_dirs
830
+ if reasoning_effort and reasoning_effort != "none":
831
+ thread_options["model_reasoning_effort"] = reasoning_effort
832
+
833
+ codex_options: dict[str, Any] = {"env": os.environ.copy()}
834
+ codex_path = str(config.get("path") or "").strip()
835
+ if codex_path:
836
+ codex_options["codexPathOverride"] = codex_path
837
+ codex = Codex(codex_options)
838
+ thread = codex.start_thread(thread_options)
839
+ turn = await thread.run(prompt, {"output_schema": _strict_schema(ANSWER_SCHEMA)})
840
+ result_text = str(getattr(turn, "final_response", "") or "")
841
+ parsed: Any = None
842
+ parse_error = ""
843
+ response = ""
844
+ if result_text.strip():
845
+ try:
846
+ parsed = json.loads(result_text)
847
+ response, parse_error = _structured_response(parsed)
848
+ except Exception as exc: # noqa: BLE001
849
+ parse_error = f"{type(exc).__name__}: {exc}"
850
+ else:
851
+ parse_error = "No response from Codex SDK (final_response is empty)."
852
+ raw = _json_dumps({
853
+ "backend": "codex_sdk",
854
+ "id": getattr(turn, "id", ""),
855
+ "thread_id": getattr(turn, "thread_id", ""),
856
+ "model": model,
857
+ "thread_options": thread_options,
858
+ "final_response": result_text,
859
+ "raw_structured_output": parsed,
860
+ "parse_error": parse_error,
861
+ "is_error": bool(parse_error),
862
+ "items": getattr(turn, "items", []),
863
+ })
864
+ return response, raw
865
+
866
+ return _run_async(asyncio.wait_for(_query(), timeout=timeout))
867
+
868
+
869
+ def _run_codex_cli_exec(
870
+ *,
871
+ work_dir: str,
872
+ prompt: str,
873
+ model: str,
874
+ timeout: int,
875
+ images: list[str] | None = None,
876
+ data_dirs: list[str] | None = None,
877
+ sandbox: str | None = None,
878
+ full_auto: bool | None = None,
879
+ ) -> tuple[str, str]:
880
+ config = get_codex_exec_config()
881
+ last_message_path = os.path.join(work_dir, "codex_last_message.txt")
882
+ cmd = [
883
+ str(config["path"]),
884
+ "exec",
885
+ "--skip-git-repo-check",
886
+ "--color",
887
+ "never",
888
+ "-C",
889
+ work_dir,
890
+ ]
891
+ if config.get("profile"):
892
+ cmd.extend(["-p", str(config["profile"])])
893
+ reasoning_effort = str(config.get("reasoning_effort", "")).strip()
894
+ if reasoning_effort:
895
+ cmd.extend(["-c", f'model_reasoning_effort="{reasoning_effort}"'])
896
+ actual_full_auto = bool(config.get("full_auto", True)) if full_auto is None else bool(full_auto)
897
+ actual_sandbox = str(sandbox or config["sandbox"])
898
+ if actual_full_auto:
899
+ cmd.append("--full-auto")
900
+ else:
901
+ cmd.extend(["--sandbox", actual_sandbox])
902
+ if model:
903
+ cmd.extend(["-m", model])
904
+ for data_dir in data_dirs or []:
905
+ _validate_exec_path(data_dir)
906
+ for image in images or []:
907
+ _validate_exec_path(os.path.dirname(image) or work_dir)
908
+ cmd.extend(["-i", image])
909
+ cmd.extend(["--output-last-message", last_message_path, prompt])
910
+
911
+ try:
912
+ proc = subprocess.run(
913
+ cmd,
914
+ cwd=work_dir,
915
+ capture_output=True,
916
+ text=True,
917
+ timeout=timeout,
918
+ )
919
+ except subprocess.TimeoutExpired as exc:
920
+ stdout = exc.stdout or ""
921
+ stderr = exc.stderr or ""
922
+ raw = stdout
923
+ if stderr:
924
+ raw = f"{raw}\n[stderr]\n{stderr}" if raw else stderr
925
+ _persist_codex_artifacts(work_dir, raw, "")
926
+ raise
927
+ try:
928
+ from skillopt.model import azure_openai as _openai
929
+ _openai.tracker.record("rollout", 0, 0)
930
+ except Exception:
931
+ pass
932
+ stdout = proc.stdout or ""
933
+ stderr = proc.stderr or ""
934
+ last_message = ""
935
+ if os.path.exists(last_message_path):
936
+ with open(last_message_path, encoding="utf-8") as f:
937
+ last_message = f.read()
938
+ raw = stdout
939
+ if stderr:
940
+ raw = f"{raw}\n[stderr]\n{stderr}" if raw else stderr
941
+ if proc.returncode != 0:
942
+ _persist_codex_artifacts(work_dir, raw, last_message)
943
+ detail = (stderr or stdout).strip()
944
+ raise RuntimeError(
945
+ f"codex exec failed with exit code {proc.returncode}: {detail[:4000]}"
946
+ )
947
+ return last_message, raw
948
+
949
+
950
+ def run_codex_exec(
951
+ *,
952
+ work_dir: str,
953
+ prompt: str,
954
+ model: str,
955
+ timeout: int,
956
+ images: list[str] | None = None,
957
+ data_dirs: list[str] | None = None,
958
+ sandbox: str | None = None,
959
+ full_auto: bool | None = None,
960
+ ) -> tuple[str, str]:
961
+ config = get_codex_exec_config()
962
+ mode = _sdk_mode(config.get("use_sdk"))
963
+ retries = int(config.get("empty_response_retries", 0) or 0)
964
+ last_response = ""
965
+ all_raw: list[str] = []
966
+
967
+ for attempt in range(retries + 1):
968
+ attempt_prompt = _retry_prompt(prompt, attempt)
969
+ if mode != "cli":
970
+ try:
971
+ response, raw = _run_codex_sdk_exec(
972
+ work_dir=work_dir,
973
+ prompt=attempt_prompt,
974
+ model=model,
975
+ timeout=timeout,
976
+ images=images,
977
+ data_dirs=data_dirs,
978
+ )
979
+ all_raw.append(f"===== CODEX SDK ATTEMPT {attempt + 1} =====\n{raw}")
980
+ if response.strip():
981
+ combined = "\n\n".join(all_raw)
982
+ _persist_codex_artifacts(work_dir, combined, response)
983
+ return response, combined
984
+ except (ImportError, ModuleNotFoundError) as exc:
985
+ raw = _raw_exception("codex_sdk", exc)
986
+ all_raw.append(f"===== CODEX SDK ATTEMPT {attempt + 1} =====\n{raw}")
987
+ if mode == "sdk":
988
+ _persist_codex_artifacts(work_dir, "\n\n".join(all_raw), "")
989
+ raise
990
+ except Exception as exc: # noqa: BLE001
991
+ raw = _raw_exception("codex_sdk", exc)
992
+ all_raw.append(f"===== CODEX SDK ATTEMPT {attempt + 1} =====\n{raw}")
993
+ if mode == "sdk" and attempt >= retries:
994
+ _persist_codex_artifacts(work_dir, "\n\n".join(all_raw), "")
995
+ raise
996
+ if mode != "sdk":
997
+ response, raw = _run_codex_cli_exec(
998
+ work_dir=work_dir,
999
+ prompt=attempt_prompt,
1000
+ model=model,
1001
+ timeout=timeout,
1002
+ images=images,
1003
+ data_dirs=data_dirs,
1004
+ sandbox=sandbox,
1005
+ full_auto=full_auto,
1006
+ )
1007
+ all_raw.append(f"===== CODEX CLI ATTEMPT {attempt + 1} =====\n{raw}")
1008
+ last_response = response
1009
+ if response.strip():
1010
+ combined = "\n\n".join(all_raw)
1011
+ _persist_codex_artifacts(work_dir, combined, response)
1012
+ return response, combined
1013
+
1014
+ combined = "\n\n".join(all_raw)
1015
+ _persist_codex_artifacts(work_dir, combined, last_response)
1016
+ return last_response, combined
1017
+
1018
+
1019
+ def run_target_exec(
1020
+ *,
1021
+ work_dir: str,
1022
+ prompt: str,
1023
+ model: str,
1024
+ timeout: int,
1025
+ images: list[str] | None = None,
1026
+ data_dirs: list[str] | None = None,
1027
+ allowed_tools: list[str] | str | None = None,
1028
+ permission_mode: str | None = None,
1029
+ sandbox: str | None = None,
1030
+ full_auto: bool | None = None,
1031
+ allow_file_edits: bool = False,
1032
+ ) -> tuple[str, str]:
1033
+ backend = get_target_backend()
1034
+ if backend == "codex_exec":
1035
+ return run_codex_exec(
1036
+ work_dir=work_dir,
1037
+ prompt=prompt,
1038
+ model=model,
1039
+ timeout=timeout,
1040
+ images=images,
1041
+ data_dirs=data_dirs,
1042
+ sandbox=sandbox,
1043
+ full_auto=full_auto,
1044
+ )
1045
+ if backend == "claude_code_exec":
1046
+ return run_claude_code_exec(
1047
+ work_dir=work_dir,
1048
+ prompt=prompt,
1049
+ model=model,
1050
+ timeout=timeout,
1051
+ images=images,
1052
+ data_dirs=data_dirs,
1053
+ allowed_tools=allowed_tools,
1054
+ permission_mode=permission_mode,
1055
+ allow_file_edits=allow_file_edits,
1056
+ )
1057
+ raise ValueError(f"Unsupported exec backend: {backend}")