synth-ai 0.2.9.dev5__py3-none-any.whl → 0.2.10__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.

Potentially problematic release.


This version of synth-ai might be problematic. Click here for more details.

Files changed (349) hide show
  1. examples/__init__.py +16 -0
  2. examples/crafter_debug_render.py +23 -17
  3. examples/dev/qwen3_32b_qlora_4xh100.toml +40 -0
  4. examples/multi_step/crafter_rl_lora.md +29 -0
  5. examples/qwen_coder/README.md +102 -0
  6. examples/qwen_coder/_shared.py +113 -0
  7. examples/qwen_coder/configs/coder_lora_30b.toml +61 -0
  8. examples/qwen_coder/configs/coder_lora_4b.toml +57 -0
  9. examples/qwen_coder/configs/coder_lora_small.toml +58 -0
  10. examples/qwen_coder/generate_dataset.py +98 -0
  11. examples/qwen_coder/infer_ft_smoke.py +65 -0
  12. examples/qwen_coder/infer_prod_proxy.py +73 -0
  13. examples/qwen_coder/infer_via_synth.py +87 -0
  14. examples/qwen_coder/scripts/infer_coder.sh +19 -0
  15. examples/qwen_coder/scripts/train_coder_30b.sh +22 -0
  16. examples/qwen_coder/sft_full_17b.py +103 -0
  17. examples/qwen_coder/sft_lora_30b.py +110 -0
  18. examples/qwen_coder/subset_jsonl.py +39 -0
  19. examples/qwen_coder/todos.md +38 -0
  20. examples/qwen_coder/validate_jsonl.py +60 -0
  21. examples/rl/configs/eval_base_qwen.toml +1 -1
  22. examples/rl/configs/rl_from_base_qwen17.toml +1 -1
  23. examples/rl/download_dataset.py +26 -10
  24. examples/rl/run_eval.py +53 -52
  25. examples/rl/run_rl_and_save.py +29 -12
  26. examples/rl/task_app/math_single_step.py +180 -41
  27. examples/rl/task_app/math_task_app.py +14 -6
  28. examples/sft/README.md +139 -0
  29. examples/sft/configs/crafter_fft_qwen0p6b.toml +44 -0
  30. examples/sft/configs/crafter_lora_qwen0p6b.toml +45 -0
  31. examples/sft/evaluate.py +117 -0
  32. examples/sft/export_dataset.py +117 -0
  33. examples/sft/generate_traces.py +162 -0
  34. examples/swe/__init__.py +12 -0
  35. examples/swe/task_app/README.md +105 -0
  36. examples/swe/task_app/__init__.py +2 -0
  37. examples/swe/task_app/grpo_swe_mini.py +571 -0
  38. examples/swe/task_app/grpo_swe_mini_task_app.py +136 -0
  39. examples/swe/task_app/hosted/README.md +173 -0
  40. examples/swe/task_app/hosted/__init__.py +5 -0
  41. examples/swe/task_app/hosted/branching.py +143 -0
  42. examples/swe/task_app/hosted/environment_routes.py +1289 -0
  43. examples/swe/task_app/hosted/envs/__init__.py +1 -0
  44. examples/swe/task_app/hosted/envs/crafter/__init__.py +6 -0
  45. examples/swe/task_app/hosted/envs/crafter/app.py +1 -0
  46. examples/swe/task_app/hosted/envs/crafter/environment.py +522 -0
  47. examples/swe/task_app/hosted/envs/crafter/policy.py +478 -0
  48. examples/swe/task_app/hosted/envs/crafter/react_agent.py +108 -0
  49. examples/swe/task_app/hosted/envs/crafter/shared.py +305 -0
  50. examples/swe/task_app/hosted/envs/crafter/tools.py +47 -0
  51. examples/swe/task_app/hosted/envs/mini_swe/__init__.py +8 -0
  52. examples/swe/task_app/hosted/envs/mini_swe/environment.py +1164 -0
  53. examples/swe/task_app/hosted/envs/mini_swe/policy.py +355 -0
  54. examples/swe/task_app/hosted/envs/mini_swe/shared.py +83 -0
  55. examples/swe/task_app/hosted/envs/mini_swe/tools.py +96 -0
  56. examples/swe/task_app/hosted/hosted_app.py +204 -0
  57. examples/swe/task_app/hosted/inference/__init__.py +5 -0
  58. examples/swe/task_app/hosted/inference/openai_client.py +618 -0
  59. examples/swe/task_app/hosted/main.py +100 -0
  60. examples/swe/task_app/hosted/policy_routes.py +1079 -0
  61. examples/swe/task_app/hosted/registry.py +195 -0
  62. examples/swe/task_app/hosted/rollout.py +1869 -0
  63. examples/swe/task_app/hosted/storage/__init__.py +5 -0
  64. examples/swe/task_app/hosted/storage/volume.py +211 -0
  65. examples/swe/task_app/hosted/test_agents.py +161 -0
  66. examples/swe/task_app/hosted/test_service.py +137 -0
  67. examples/swe/task_app/hosted/utils.py +62 -0
  68. examples/vlm/PROPOSAL.md +53 -0
  69. examples/vlm/README.md +68 -0
  70. examples/vlm/configs/crafter_vlm_gpt4o.toml +44 -0
  71. examples/vlm/crafter_image_only_agent.py +207 -0
  72. examples/vlm/crafter_openai_vlm_agent.py +277 -0
  73. examples/vlm/filter_image_rows.py +63 -0
  74. examples/vlm/run_crafter_vlm_benchmark.py +316 -0
  75. examples/warming_up_to_rl/analyze_trace_db.py +12 -10
  76. examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +11 -1
  77. examples/warming_up_to_rl/export_trace_sft.py +218 -36
  78. examples/warming_up_to_rl/groq_test.py +15 -8
  79. examples/warming_up_to_rl/manage_secrets.py +29 -25
  80. examples/warming_up_to_rl/readme.md +9 -2
  81. examples/warming_up_to_rl/run_eval.py +137 -61
  82. examples/warming_up_to_rl/run_fft_and_save.py +131 -60
  83. examples/warming_up_to_rl/run_local_rollout.py +88 -39
  84. examples/warming_up_to_rl/run_local_rollout_modal.py +114 -28
  85. examples/warming_up_to_rl/run_local_rollout_parallel.py +81 -20
  86. examples/warming_up_to_rl/run_local_rollout_traced.py +126 -23
  87. examples/warming_up_to_rl/run_rl_and_save.py +35 -12
  88. examples/warming_up_to_rl/run_rollout_remote.py +44 -19
  89. examples/warming_up_to_rl/task_app/README.md +6 -2
  90. examples/warming_up_to_rl/task_app/grpo_crafter.py +319 -57
  91. examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +11 -30
  92. examples/warming_up_to_rl/task_app/synth_envs_hosted/__init__.py +1 -1
  93. examples/warming_up_to_rl/task_app/synth_envs_hosted/branching.py +9 -11
  94. examples/warming_up_to_rl/task_app/synth_envs_hosted/environment_routes.py +137 -182
  95. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/__init__.py +1 -1
  96. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/__init__.py +1 -1
  97. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/app.py +1 -1
  98. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/environment.py +150 -57
  99. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +105 -69
  100. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +19 -7
  101. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/shared.py +45 -42
  102. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/tools.py +1 -1
  103. examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +47 -45
  104. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/__init__.py +1 -1
  105. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +198 -92
  106. examples/warming_up_to_rl/task_app/synth_envs_hosted/main.py +0 -2
  107. examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +361 -263
  108. examples/warming_up_to_rl/task_app/synth_envs_hosted/registry.py +21 -23
  109. examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +394 -274
  110. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/__init__.py +1 -1
  111. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/volume.py +56 -62
  112. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_agents.py +1 -0
  113. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_service.py +6 -15
  114. examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +4 -3
  115. synth_ai/__init__.py +1 -0
  116. synth_ai/api/models/supported.py +376 -0
  117. synth_ai/api/train/builders.py +157 -26
  118. synth_ai/api/train/cli.py +213 -57
  119. synth_ai/api/train/config_finder.py +65 -5
  120. synth_ai/api/train/env_resolver.py +33 -15
  121. synth_ai/api/train/pollers.py +13 -4
  122. synth_ai/api/train/supported_algos.py +139 -0
  123. synth_ai/api/train/task_app.py +5 -3
  124. synth_ai/api/train/utils.py +33 -48
  125. synth_ai/cli/__init__.py +19 -4
  126. synth_ai/cli/_modal_wrapper.py +28 -0
  127. synth_ai/cli/_typer_patch.py +49 -0
  128. synth_ai/cli/balance.py +2 -3
  129. synth_ai/cli/calc.py +1 -1
  130. synth_ai/cli/demo.py +21 -6
  131. synth_ai/cli/recent.py +2 -2
  132. synth_ai/cli/rl_demo.py +77 -17
  133. synth_ai/cli/root.py +116 -39
  134. synth_ai/cli/status.py +2 -2
  135. synth_ai/cli/task_apps.py +1699 -259
  136. synth_ai/cli/traces.py +7 -4
  137. synth_ai/cli/turso.py +73 -0
  138. synth_ai/cli/watch.py +12 -18
  139. synth_ai/core/experiment.py +0 -2
  140. synth_ai/demo_registry.py +68 -31
  141. synth_ai/demos/core/cli.py +516 -194
  142. synth_ai/demos/demo_task_apps/__init__.py +3 -3
  143. synth_ai/demos/demo_task_apps/core.py +64 -28
  144. synth_ai/demos/demo_task_apps/crafter/configs/crafter_fft_4b.toml +2 -3
  145. synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +37 -30
  146. synth_ai/demos/demo_task_apps/math/_common.py +1 -2
  147. synth_ai/demos/demo_task_apps/math/app.py +2 -1
  148. synth_ai/demos/demo_task_apps/math/deploy_modal.py +3 -6
  149. synth_ai/demos/demo_task_apps/math/modal_task_app.py +183 -82
  150. synth_ai/demos/demo_task_apps/math/task_app_entry.py +0 -2
  151. synth_ai/environments/examples/bandit/engine.py +12 -4
  152. synth_ai/environments/examples/bandit/taskset.py +4 -4
  153. synth_ai/environments/examples/crafter_classic/environment.py +76 -1
  154. synth_ai/environments/reproducibility/tree.py +5 -6
  155. synth_ai/environments/service/app.py +11 -12
  156. synth_ai/environments/service/core_routes.py +10 -9
  157. synth_ai/environments/stateful/engine.py +1 -1
  158. synth_ai/environments/tasks/core.py +1 -0
  159. synth_ai/environments/tasks/filters.py +5 -6
  160. synth_ai/environments/tasks/utils.py +4 -5
  161. synth_ai/evals/base.py +0 -2
  162. synth_ai/handshake.py +11 -9
  163. synth_ai/http.py +1 -1
  164. synth_ai/http_client.py +43 -11
  165. synth_ai/inference/__init__.py +0 -2
  166. synth_ai/inference/client.py +20 -6
  167. synth_ai/jobs/client.py +103 -78
  168. synth_ai/learning/__init__.py +41 -6
  169. synth_ai/learning/algorithms.py +14 -0
  170. synth_ai/learning/client.py +121 -29
  171. synth_ai/learning/config.py +2 -40
  172. synth_ai/learning/constants.py +0 -2
  173. synth_ai/learning/ft_client.py +4 -56
  174. synth_ai/learning/health.py +13 -7
  175. synth_ai/learning/jobs.py +43 -47
  176. synth_ai/{rl → learning/rl}/__init__.py +14 -5
  177. synth_ai/learning/rl/client.py +267 -0
  178. synth_ai/learning/rl/config.py +31 -0
  179. synth_ai/{rl → learning/rl}/contracts.py +5 -10
  180. synth_ai/{rl → learning/rl}/env_keys.py +45 -16
  181. synth_ai/learning/rl/secrets.py +13 -0
  182. synth_ai/learning/rl_client.py +2 -253
  183. synth_ai/learning/sft/__init__.py +29 -0
  184. synth_ai/learning/sft/client.py +68 -0
  185. synth_ai/learning/sft/config.py +270 -0
  186. synth_ai/learning/sft/data.py +295 -0
  187. synth_ai/learning/sse.py +25 -26
  188. synth_ai/learning/validators.py +25 -24
  189. synth_ai/lm/__init__.py +21 -47
  190. synth_ai/task/__init__.py +26 -27
  191. synth_ai/task/apps/__init__.py +18 -19
  192. synth_ai/task/auth.py +35 -23
  193. synth_ai/task/client.py +15 -13
  194. synth_ai/task/contracts.py +37 -35
  195. synth_ai/task/datasets.py +9 -6
  196. synth_ai/task/errors.py +11 -10
  197. synth_ai/task/health.py +17 -11
  198. synth_ai/task/json.py +58 -24
  199. synth_ai/task/proxy.py +15 -14
  200. synth_ai/task/rubrics.py +22 -15
  201. synth_ai/task/server.py +43 -17
  202. synth_ai/task/tracing_utils.py +12 -7
  203. synth_ai/task/validators.py +0 -1
  204. synth_ai/task/vendors.py +5 -7
  205. synth_ai/tracing_v3/__init__.py +2 -0
  206. synth_ai/tracing_v3/abstractions.py +21 -4
  207. synth_ai/tracing_v3/db_config.py +26 -1
  208. synth_ai/tracing_v3/decorators.py +18 -15
  209. synth_ai/tracing_v3/examples/basic_usage.py +3 -2
  210. synth_ai/tracing_v3/hooks.py +6 -4
  211. synth_ai/tracing_v3/llm_call_record_helpers.py +6 -6
  212. synth_ai/tracing_v3/replica_sync.py +1 -0
  213. synth_ai/tracing_v3/session_tracer.py +63 -16
  214. synth_ai/tracing_v3/storage/base.py +89 -1
  215. synth_ai/tracing_v3/storage/config.py +21 -8
  216. synth_ai/tracing_v3/storage/factory.py +10 -8
  217. synth_ai/tracing_v3/storage/utils.py +4 -2
  218. synth_ai/tracing_v3/turso/daemon.py +7 -2
  219. synth_ai/tracing_v3/turso/models.py +5 -2
  220. synth_ai/tracing_v3/turso/native_manager.py +1173 -0
  221. synth_ai/tracing_v3/utils.py +4 -3
  222. synth_ai/v0/api/__init__.py +8 -0
  223. synth_ai/v0/api/models/__init__.py +8 -0
  224. synth_ai/v0/api/models/supported.py +8 -0
  225. synth_ai/v0/config/__init__.py +15 -0
  226. synth_ai/v0/config/base_url.py +12 -0
  227. synth_ai/v0/lm/__init__.py +51 -0
  228. synth_ai/{lm → v0/lm}/caching/ephemeral.py +3 -5
  229. synth_ai/{lm → v0/lm}/caching/handler.py +4 -4
  230. synth_ai/{lm → v0/lm}/caching/initialize.py +1 -1
  231. synth_ai/{lm → v0/lm}/caching/persistent.py +1 -1
  232. synth_ai/{lm → v0/lm}/config.py +6 -1
  233. synth_ai/{lm → v0/lm}/core/all.py +9 -9
  234. synth_ai/{lm → v0/lm}/core/exceptions.py +0 -2
  235. synth_ai/{lm → v0/lm}/core/main.py +19 -7
  236. synth_ai/{lm → v0/lm}/core/main_v3.py +10 -10
  237. synth_ai/{lm → v0/lm}/core/synth_models.py +2 -15
  238. synth_ai/{lm → v0/lm}/core/vendor_clients.py +6 -4
  239. synth_ai/{lm → v0/lm}/overrides.py +4 -4
  240. synth_ai/{lm → v0/lm}/provider_support/anthropic.py +4 -4
  241. synth_ai/{lm → v0/lm}/provider_support/openai.py +5 -5
  242. synth_ai/{lm → v0/lm}/structured_outputs/handler.py +5 -5
  243. synth_ai/{lm → v0/lm}/structured_outputs/rehabilitate.py +1 -1
  244. synth_ai/{lm → v0/lm}/vendors/core/anthropic_api.py +16 -16
  245. synth_ai/{lm → v0/lm}/vendors/core/gemini_api.py +5 -5
  246. synth_ai/{lm → v0/lm}/vendors/core/mistral_api.py +5 -5
  247. synth_ai/{lm → v0/lm}/vendors/core/openai_api.py +12 -10
  248. synth_ai/{lm → v0/lm}/vendors/openai_standard.py +11 -9
  249. synth_ai/{lm → v0/lm}/vendors/openai_standard_responses.py +8 -5
  250. synth_ai/{lm → v0/lm}/vendors/supported/custom_endpoint.py +4 -6
  251. synth_ai/{lm → v0/lm}/vendors/supported/deepseek.py +2 -2
  252. synth_ai/{lm → v0/lm}/vendors/supported/grok.py +2 -2
  253. synth_ai/{lm → v0/lm}/vendors/supported/groq.py +1 -1
  254. synth_ai/{lm → v0/lm}/vendors/supported/ollama.py +1 -1
  255. synth_ai/{lm → v0/lm}/vendors/supported/openrouter.py +3 -3
  256. synth_ai/{lm → v0/lm}/vendors/supported/together.py +1 -1
  257. synth_ai/{lm → v0/lm}/vendors/synth_client.py +38 -11
  258. synth_ai/v0/tracing/upload.py +32 -135
  259. synth_ai/v0/tracing_v3/__init__.py +10 -0
  260. synth_ai/v0/tracing_v3/abstractions.py +3 -0
  261. synth_ai/v0/tracing_v3/decorators.py +3 -0
  262. synth_ai/v0/tracing_v3/llm_call_record_helpers.py +3 -0
  263. synth_ai/v0/tracing_v3/session_tracer.py +3 -0
  264. {synth_ai-0.2.9.dev5.dist-info → synth_ai-0.2.10.dist-info}/METADATA +10 -7
  265. {synth_ai-0.2.9.dev5.dist-info → synth_ai-0.2.10.dist-info}/RECORD +294 -258
  266. examples/common_old/backend.py +0 -21
  267. examples/evals_old/README.md +0 -98
  268. examples/evals_old/__init__.py +0 -6
  269. examples/evals_old/compare_models.py +0 -1037
  270. examples/evals_old/example_log.md +0 -145
  271. examples/evals_old/run_demo.sh +0 -126
  272. examples/evals_old/trace_analysis.py +0 -270
  273. examples/finetuning_old/_backup_synth_qwen/config.toml +0 -29
  274. examples/finetuning_old/_backup_synth_qwen/example_log.md +0 -324
  275. examples/finetuning_old/_backup_synth_qwen/filter_traces.py +0 -60
  276. examples/finetuning_old/_backup_synth_qwen/filter_traces_achievements.py +0 -239
  277. examples/finetuning_old/_backup_synth_qwen/purge_v3_traces.py +0 -109
  278. examples/finetuning_old/_backup_synth_qwen/react_agent_lm.py +0 -1924
  279. examples/finetuning_old/_backup_synth_qwen/readme.md +0 -49
  280. examples/finetuning_old/_backup_synth_qwen/run_crafter_qwen4b.py +0 -114
  281. examples/finetuning_old/_backup_synth_qwen/run_demo.sh +0 -195
  282. examples/finetuning_old/_backup_synth_qwen/sft_kickoff.py +0 -118
  283. examples/finetuning_old/synth_qwen_v1/README.md +0 -68
  284. examples/finetuning_old/synth_qwen_v1/filter_traces.py +0 -60
  285. examples/finetuning_old/synth_qwen_v1/filter_traces_achievements.py +0 -239
  286. examples/finetuning_old/synth_qwen_v1/finetune.py +0 -46
  287. examples/finetuning_old/synth_qwen_v1/hello_ft_model.py +0 -71
  288. examples/finetuning_old/synth_qwen_v1/infer.py +0 -37
  289. examples/finetuning_old/synth_qwen_v1/poll.py +0 -44
  290. examples/finetuning_old/synth_qwen_v1/prepare_data.py +0 -35
  291. examples/finetuning_old/synth_qwen_v1/purge_v3_traces.py +0 -109
  292. examples/finetuning_old/synth_qwen_v1/react_agent_lm.py +0 -1932
  293. examples/finetuning_old/synth_qwen_v1/run_crafter_sft_job.py +0 -207
  294. examples/finetuning_old/synth_qwen_v1/run_ft_job.py +0 -232
  295. examples/finetuning_old/synth_qwen_v1/upload_data.py +0 -34
  296. examples/finetuning_old/synth_qwen_v1/util.py +0 -147
  297. examples/rl_old/task_app.py +0 -962
  298. synth_ai/experimental/synth_oss.py +0 -446
  299. synth_ai/install_sqld.sh +0 -40
  300. synth_ai/learning/filtering.py +0 -0
  301. synth_ai/learning/offline/dpo.py +0 -0
  302. synth_ai/learning/offline/providers.py +0 -7
  303. synth_ai/learning/offline/sft.py +0 -0
  304. synth_ai/learning/offline/shared.py +0 -0
  305. synth_ai/learning/online/grpo.py +0 -0
  306. synth_ai/learning/online/irft.py +0 -0
  307. synth_ai/learning/prompts/banking77_injection_eval.py +0 -168
  308. synth_ai/learning/prompts/gepa.py +0 -0
  309. synth_ai/learning/prompts/hello_world_in_context_injection_ex.py +0 -213
  310. synth_ai/learning/prompts/mipro.py +0 -289
  311. synth_ai/learning/prompts/random_search.py +0 -246
  312. synth_ai/learning/prompts/run_mipro_banking77.py +0 -172
  313. synth_ai/learning/prompts/run_random_search_banking77.py +0 -324
  314. synth_ai/rl/secrets.py +0 -19
  315. synth_ai/scripts/verify_rewards.py +0 -100
  316. synth_ai/tracing/__init__.py +0 -30
  317. synth_ai/tracing_v1/__init__.py +0 -33
  318. synth_ai/tracing_v3/turso/__init__.py +0 -25
  319. synth_ai/tracing_v3/turso/manager.py +0 -774
  320. synth_ai/zyk/__init__.py +0 -30
  321. /synth_ai/{lm → v0/lm}/caching/__init__.py +0 -0
  322. /synth_ai/{lm → v0/lm}/caching/constants.py +0 -0
  323. /synth_ai/{lm → v0/lm}/caching/dbs.py +0 -0
  324. /synth_ai/{lm → v0/lm}/constants.py +0 -0
  325. /synth_ai/{lm → v0/lm}/core/__init__.py +0 -0
  326. /synth_ai/{lm → v0/lm}/cost/__init__.py +0 -0
  327. /synth_ai/{lm → v0/lm}/cost/monitor.py +0 -0
  328. /synth_ai/{lm → v0/lm}/cost/statefulness.py +0 -0
  329. /synth_ai/{lm → v0/lm}/injection.py +0 -0
  330. /synth_ai/{lm → v0/lm}/provider_support/__init__.py +0 -0
  331. /synth_ai/{lm → v0/lm}/provider_support/suppress_logging.py +0 -0
  332. /synth_ai/{lm → v0/lm}/structured_outputs/__init__.py +0 -0
  333. /synth_ai/{lm → v0/lm}/structured_outputs/inject.py +0 -0
  334. /synth_ai/{lm → v0/lm}/tools/__init__.py +0 -0
  335. /synth_ai/{lm → v0/lm}/tools/base.py +0 -0
  336. /synth_ai/{lm → v0/lm}/unified_interface.py +0 -0
  337. /synth_ai/{lm → v0/lm}/vendors/__init__.py +0 -0
  338. /synth_ai/{lm → v0/lm}/vendors/base.py +0 -0
  339. /synth_ai/{lm → v0/lm}/vendors/core/__init__.py +0 -0
  340. /synth_ai/{lm → v0/lm}/vendors/core/synth_dev_api.py +0 -0
  341. /synth_ai/{lm → v0/lm}/vendors/local/__init__.py +0 -0
  342. /synth_ai/{lm → v0/lm}/vendors/local/ollama.py +0 -0
  343. /synth_ai/{lm → v0/lm}/vendors/retries.py +0 -0
  344. /synth_ai/{lm → v0/lm}/vendors/supported/__init__.py +0 -0
  345. /synth_ai/{lm → v0/lm}/warmup.py +0 -0
  346. {synth_ai-0.2.9.dev5.dist-info → synth_ai-0.2.10.dist-info}/WHEEL +0 -0
  347. {synth_ai-0.2.9.dev5.dist-info → synth_ai-0.2.10.dist-info}/entry_points.txt +0 -0
  348. {synth_ai-0.2.9.dev5.dist-info → synth_ai-0.2.10.dist-info}/licenses/LICENSE +0 -0
  349. {synth_ai-0.2.9.dev5.dist-info → synth_ai-0.2.10.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1164 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import json
6
+ import logging
7
+ import os
8
+ import shlex
9
+ import shutil
10
+ import subprocess
11
+ import threading
12
+ import time
13
+ import uuid
14
+ from dataclasses import dataclass, field
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from minisweagent.environments import get_environment
19
+ from synth_ai.environments.environment.tools import EnvToolCall
20
+
21
+ from .shared import summarise_history
22
+ from .tools import TOOLS_SCHEMA
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ def _environment_type_from_config(config: dict[str, Any]) -> str:
28
+ value = (config or {}).get("environment_class") or os.getenv(
29
+ "SWE_MINI_ENVIRONMENT_CLASS", "local"
30
+ )
31
+ return str(value).strip() or "local"
32
+
33
+
34
+ def _environment_kwargs_from_config(config: dict[str, Any]) -> dict[str, Any]:
35
+ kwargs = dict(config or {}).get("environment_kwargs") or {}
36
+ if not kwargs and (raw := os.getenv("SWE_MINI_ENVIRONMENT_KWARGS")):
37
+ try:
38
+ kwargs = json.loads(raw)
39
+ except Exception: # pragma: no cover - environment var malformed
40
+ logger.warning("Failed to parse SWE_MINI_ENVIRONMENT_KWARGS; ignoring")
41
+ kwargs = {}
42
+ if not isinstance(kwargs, dict):
43
+ logger.warning("environment_kwargs must be a mapping, got %r", type(kwargs))
44
+ kwargs = {}
45
+ return kwargs
46
+
47
+
48
+ def _default_submit_command() -> str:
49
+ return "echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT && git add -A && git diff --cached"
50
+
51
+
52
+ @dataclass
53
+ class MiniSweEnvironmentState:
54
+ """Serializable environment state used for snapshots."""
55
+
56
+ task: dict[str, Any]
57
+ history: list[dict[str, Any]] = field(default_factory=list)
58
+ step_idx: int = 0
59
+ submitted: bool = False
60
+ submission_success: bool | None = None
61
+
62
+
63
+ class MiniSweEnvironmentWrapper:
64
+ """Wrapper around mini-swe-agent environments exposing Synth task-app semantics."""
65
+
66
+ name = "swe-mini"
67
+
68
+ def __init__(
69
+ self,
70
+ *,
71
+ task: dict[str, Any],
72
+ env_config: dict[str, Any] | None = None,
73
+ submit_command: str | None = None,
74
+ ) -> None:
75
+ self.task = dict(task)
76
+ self.env_config = dict(env_config or {})
77
+ self.submit_command = submit_command or _default_submit_command()
78
+ self.environment_type = _environment_type_from_config(self.env_config)
79
+ kwargs = _environment_kwargs_from_config(self.env_config)
80
+
81
+ self.instance_id = str(
82
+ self.task.get("instance_id") or f"swe-mini-{uuid.uuid4().hex[:8]}"
83
+ )
84
+ self.metadata = dict(self.task.get("metadata") or {})
85
+ self.repo_url = self._resolve_repo_url(self.metadata)
86
+ self.base_commit = (
87
+ self.metadata.get("base_commit")
88
+ or self.metadata.get("environment_setup_commit")
89
+ or None
90
+ )
91
+ self._local_workspace_dir: Path | None = None
92
+ self._remote_workspace: str | None = None
93
+ self._cleanup_workspace = False
94
+
95
+ if self.environment_type == "local":
96
+ workspace = self._prepare_local_workspace(kwargs)
97
+ kwargs.setdefault("cwd", str(workspace))
98
+ kwargs.setdefault("timeout", int(self.env_config.get("timeout", 60)))
99
+ # Merge custom env vars with defaults expected by mini-swe
100
+ merged_env = dict(kwargs.get("env") or {})
101
+ merged_env.setdefault("PAGER", "cat")
102
+ merged_env.setdefault("MANPAGER", "cat")
103
+ merged_env.setdefault("LESS", "-R")
104
+ merged_env.setdefault("PIP_PROGRESS_BAR", "off")
105
+ merged_env.setdefault("TQDM_DISABLE", "1")
106
+ merged_env.setdefault("GIT_TERMINAL_PROMPT", "0")
107
+ kwargs["env"] = merged_env
108
+ self._local_workspace_dir = workspace
109
+ self._cleanup_workspace = True
110
+ else:
111
+ remote_cwd = kwargs.get("cwd")
112
+ if not remote_cwd:
113
+ base_remote = os.getenv("SWE_MINI_REMOTE_WORKSPACE_BASE", "/workspace")
114
+ remote_cwd = f"{base_remote.rstrip('/')}/{self.instance_id}"
115
+ kwargs["cwd"] = remote_cwd
116
+ self._remote_workspace = kwargs["cwd"]
117
+ timeout = self.env_config.get("timeout")
118
+ if timeout and "timeout" not in kwargs:
119
+ kwargs["timeout"] = int(timeout)
120
+ if self.repo_url and "image" not in kwargs:
121
+ image = self.metadata.get("image_name") or os.getenv("SWE_MINI_DOCKER_IMAGE")
122
+ if image:
123
+ kwargs["image"] = image
124
+ if self.environment_type in {"docker", "bubblewrap"}:
125
+ remote_env = dict(kwargs.get("env") or {})
126
+ remote_env.setdefault("GIT_TERMINAL_PROMPT", "0")
127
+ kwargs["env"] = remote_env
128
+
129
+ logger.info(
130
+ "Initialising mini-swe environment: type=%s kwargs=%s",
131
+ self.environment_type,
132
+ kwargs,
133
+ )
134
+ self.env = get_environment(
135
+ {
136
+ "environment_class": self.environment_type,
137
+ **kwargs,
138
+ },
139
+ default_type="local",
140
+ )
141
+
142
+ if self.environment_type != "local":
143
+ self._bootstrap_remote_workspace()
144
+
145
+ self.state = MiniSweEnvironmentState(task=self.task)
146
+ self.last_result: dict[str, Any] | None = None
147
+ self.last_submission: dict[str, Any] | None = None
148
+
149
+ async def initialize(self) -> dict[str, Any]:
150
+ """Return initial observation."""
151
+ logger.info(
152
+ "Mini-swe task initialised: instance=%s",
153
+ self.task.get("instance_id"),
154
+ )
155
+ return self._build_response(observation=self._build_observation(None), step_idx=0)
156
+
157
+ async def terminate(self) -> dict[str, Any]:
158
+ """Terminate the environment, returning the final observation."""
159
+ logger.info(
160
+ "Terminating mini-swe environment instance=%s submitted=%s",
161
+ self.task.get("instance_id"),
162
+ self.state.submitted,
163
+ )
164
+ response = self._build_response(
165
+ observation=self._build_observation(self.last_result),
166
+ step_idx=self.state.step_idx,
167
+ )
168
+ self._cleanup_workspaces()
169
+ return response
170
+
171
+ def _cleanup_workspaces(self) -> None:
172
+ if self._cleanup_workspace and self._local_workspace_dir:
173
+ with contextlib.suppress(Exception):
174
+ shutil.rmtree(self._local_workspace_dir)
175
+ self._local_workspace_dir = None
176
+ self._cleanup_workspace = False
177
+ if (
178
+ self._remote_workspace
179
+ and os.getenv("SWE_MINI_CLEANUP_REMOTE_WORKSPACE", "1") not in {"0", "false", "False"}
180
+ ):
181
+ with contextlib.suppress(Exception):
182
+ self.env.execute(f"rm -rf {shlex.quote(self._remote_workspace)}")
183
+ self._remote_workspace = None
184
+
185
+ def _resolve_repo_url(self, metadata: dict[str, Any]) -> str | None:
186
+ candidates = [
187
+ metadata.get("repo_url"),
188
+ metadata.get("repo"),
189
+ metadata.get("repository"),
190
+ ]
191
+ for value in candidates:
192
+ if not value:
193
+ continue
194
+ repo = str(value).strip()
195
+ if not repo:
196
+ continue
197
+ if repo.startswith("http://") or repo.startswith("https://"):
198
+ url = repo
199
+ else:
200
+ repo = repo.removesuffix(".git")
201
+ url = f"https://github.com/{repo}.git"
202
+ if not url.endswith(".git"):
203
+ url = f"{url}.git"
204
+ return url
205
+ return None
206
+
207
+ def _prepare_local_workspace(self, kwargs: dict[str, Any]) -> Path:
208
+ if not self.repo_url:
209
+ fallback = Path(kwargs.get("cwd") or self.env_config.get("cwd") or os.getcwd())
210
+ fallback.mkdir(parents=True, exist_ok=True)
211
+ logger.warning(
212
+ "No repo URL provided for swe-mini instance %s; using cwd=%s",
213
+ self.instance_id,
214
+ fallback,
215
+ )
216
+ return fallback
217
+
218
+ root = Path(
219
+ os.getenv("SWE_MINI_LOCAL_WORKSPACE_ROOT")
220
+ or Path.home() / ".cache" / "synth-ai" / "swe-mini" / "workspaces"
221
+ )
222
+ workspace = root / self.instance_id
223
+ if workspace.exists():
224
+ shutil.rmtree(workspace, ignore_errors=True)
225
+ workspace.parent.mkdir(parents=True, exist_ok=True)
226
+
227
+ self._run_local_cmd(
228
+ [
229
+ "git",
230
+ "clone",
231
+ "--filter=blob:none",
232
+ "--no-tags",
233
+ self.repo_url,
234
+ str(workspace),
235
+ ],
236
+ description="clone repository",
237
+ )
238
+ if self.base_commit:
239
+ self._run_local_cmd(
240
+ ["git", "-C", str(workspace), "checkout", self.base_commit],
241
+ description="checkout base commit",
242
+ )
243
+ self._run_local_cmd(
244
+ ["git", "-C", str(workspace), "reset", "--hard"],
245
+ description="reset working tree",
246
+ )
247
+ self._run_local_cmd(
248
+ ["git", "-C", str(workspace), "clean", "-ffd"],
249
+ description="clean working tree",
250
+ )
251
+ logger.info(
252
+ "Prepared local workspace for %s at %s (repo=%s, commit=%s)",
253
+ self.instance_id,
254
+ workspace,
255
+ self.repo_url,
256
+ self.base_commit,
257
+ )
258
+ return workspace
259
+
260
+ def _bootstrap_remote_workspace(self) -> None:
261
+ if not self.repo_url or not self._remote_workspace:
262
+ logger.warning(
263
+ "Skipping remote workspace bootstrap for instance %s (repo=%s workspace=%s)",
264
+ self.instance_id,
265
+ self.repo_url,
266
+ self._remote_workspace,
267
+ )
268
+ return
269
+
270
+ workspace = self._remote_workspace.rstrip("/")
271
+ base_dir = os.path.dirname(workspace) or "/"
272
+ self._execute_bootstrap_command(f"mkdir -p {shlex.quote(base_dir)}")
273
+ self._execute_bootstrap_command(f"rm -rf {shlex.quote(workspace)}")
274
+ clone_cmd = (
275
+ f"git clone --filter=blob:none --no-tags {shlex.quote(self.repo_url)} {shlex.quote(workspace)}"
276
+ )
277
+ self._execute_bootstrap_command(clone_cmd, timeout=900, description="clone repository")
278
+ if self.base_commit:
279
+ checkout_cmd = (
280
+ f"cd {shlex.quote(workspace)} && git checkout {shlex.quote(self.base_commit)}"
281
+ )
282
+ self._execute_bootstrap_command(checkout_cmd, timeout=300, description="checkout commit")
283
+ self._execute_bootstrap_command(
284
+ f"cd {shlex.quote(workspace)} && git reset --hard",
285
+ description="reset working tree",
286
+ )
287
+ self._execute_bootstrap_command(
288
+ f"cd {shlex.quote(workspace)} && git clean -ffd",
289
+ description="clean working tree",
290
+ )
291
+ logger.info(
292
+ "Prepared remote workspace for %s at %s (repo=%s, commit=%s)",
293
+ self.instance_id,
294
+ workspace,
295
+ self.repo_url,
296
+ self.base_commit,
297
+ )
298
+
299
+ def _run_local_cmd(
300
+ self, args: list[str], *, cwd: Path | None = None, description: str | None = None
301
+ ) -> None:
302
+ logger.debug(
303
+ "Preparing workspace %s: running local command %s",
304
+ self.instance_id,
305
+ " ".join(args),
306
+ )
307
+ proc = subprocess.run(
308
+ args,
309
+ cwd=str(cwd) if cwd else None,
310
+ text=True,
311
+ capture_output=True,
312
+ )
313
+ if proc.returncode != 0:
314
+ desc = description or "command"
315
+ raise RuntimeError(
316
+ f"Failed to {desc} (cmd={' '.join(args)}): {proc.stdout or ''}{proc.stderr or ''}"
317
+ )
318
+
319
+ def _execute_bootstrap_command(
320
+ self, command: str, *, timeout: int | None = None, description: str | None = None
321
+ ) -> None:
322
+ logger.debug(
323
+ "Preparing workspace %s: running remote command %s",
324
+ self.instance_id,
325
+ command,
326
+ )
327
+ result = self.env.execute(command, timeout=timeout)
328
+ if result.get("returncode"):
329
+ desc = description or command
330
+ raise RuntimeError(
331
+ f"Failed to {desc}: rc={result.get('returncode')} output={result.get('output')}"
332
+ )
333
+
334
+ def _normalize_tool_call(self, tool_call: EnvToolCall | dict[str, Any]) -> EnvToolCall:
335
+ if isinstance(tool_call, EnvToolCall):
336
+ return tool_call
337
+ tool = tool_call.get("tool") or tool_call.get("tool_name")
338
+ if not tool:
339
+ raise ValueError(f"Tool call missing tool name: {tool_call}")
340
+ args = tool_call.get("args") or tool_call.get("arguments") or {}
341
+ if isinstance(args, str):
342
+ try:
343
+ args = json.loads(args)
344
+ except Exception:
345
+ args = {}
346
+ return EnvToolCall(tool=str(tool), args=dict(args))
347
+
348
+ async def step(self, tool_calls: list[EnvToolCall] | list[dict[str, Any]]) -> dict[str, Any]:
349
+ """Execute run_command or submit_patch tool calls."""
350
+ if not tool_calls:
351
+ raise ValueError("MiniSweEnvironmentWrapper.step requires at least one tool call")
352
+
353
+ responses: list[dict[str, Any]] = []
354
+ for raw_call in tool_calls:
355
+ call = self._normalize_tool_call(raw_call)
356
+ tool = call.tool
357
+ if tool == "run_command":
358
+ responses.append(self._run_command(call))
359
+ elif tool == "submit_patch":
360
+ responses.append(self._submit(call))
361
+ else:
362
+ raise ValueError(f"Unsupported tool '{tool}' for swe-mini environment")
363
+
364
+ last_result = responses[-1] if responses else None
365
+ self.last_result = last_result
366
+ observation = self._build_observation(last_result)
367
+ done = bool(self.state.submitted)
368
+ reward = 0.0
369
+ if done:
370
+ reward = 1.0 if self.state.submission_success else 0.0
371
+ return self._build_response(
372
+ observation=observation,
373
+ step_idx=self.state.step_idx,
374
+ done=done,
375
+ reward=reward,
376
+ info={"responses": responses},
377
+ )
378
+
379
+ def _run_command(self, call: EnvToolCall) -> dict[str, Any]:
380
+ command = str(call.args.get("command") or "").strip()
381
+ if not command:
382
+ raise ValueError("run_command requires a non-empty 'command' argument")
383
+ timeout = call.args.get("timeout")
384
+ timeout = int(timeout) if timeout is not None else None
385
+
386
+ started_at = time.time()
387
+ result = self.env.execute(command, timeout=timeout)
388
+ duration = time.time() - started_at
389
+
390
+ record = {
391
+ "command": command,
392
+ "returncode": result.get("returncode"),
393
+ "stdout": result.get("output") or "",
394
+ "duration": duration,
395
+ "timestamp": started_at,
396
+ }
397
+ self.state.history.append(record)
398
+ self.state.step_idx += 1
399
+ logger.info(
400
+ "Executed command step=%s rc=%s",
401
+ self.state.step_idx,
402
+ record["returncode"],
403
+ )
404
+ return record
405
+
406
+ def _submit(self, call: EnvToolCall) -> dict[str, Any]:
407
+ if self.state.submitted:
408
+ logger.info("Submit called again; ignoring additional submission.")
409
+ return {
410
+ "submitted": True,
411
+ "command": None,
412
+ "returncode": 0,
413
+ "stdout": "",
414
+ "submission_success": self.state.submission_success,
415
+ "evaluation": self.last_submission,
416
+ }
417
+ command = str(call.args.get("command") or self.submit_command)
418
+ result = self.env.execute(command)
419
+ record = {
420
+ "command": command,
421
+ "returncode": result.get("returncode"),
422
+ "stdout": result.get("output") or "",
423
+ "duration": 0.0,
424
+ "timestamp": time.time(),
425
+ }
426
+ self.state.history.append(record)
427
+ self.state.step_idx += 1
428
+ diff = self._extract_submission_diff(record["stdout"])
429
+
430
+ evaluation: dict[str, Any] | None = None
431
+ submission_success = False
432
+ if record["returncode"] == 0 and diff is not None:
433
+ evaluation = self._evaluate_submission(diff)
434
+ submission_success = bool(evaluation.get("resolved")) if evaluation else False
435
+ else:
436
+ evaluation = {
437
+ "completed": False,
438
+ "resolved": False,
439
+ "error": "submit command failed or diff unavailable",
440
+ "returncode": record["returncode"],
441
+ }
442
+
443
+ self.state.submitted = True
444
+ self.state.submission_success = submission_success
445
+ self.last_submission = evaluation
446
+
447
+ logger.info(
448
+ "Submission command executed rc=%s resolved=%s",
449
+ record["returncode"],
450
+ submission_success,
451
+ )
452
+
453
+ return {
454
+ **record,
455
+ "submitted": True,
456
+ "submission_success": submission_success,
457
+ "diff": diff,
458
+ "evaluation": evaluation,
459
+ }
460
+
461
+ def _extract_submission_diff(self, stdout: str) -> str | None:
462
+ if stdout is None:
463
+ return None
464
+ lines = stdout.splitlines()
465
+ if not lines:
466
+ return ""
467
+ first = lines[0].strip()
468
+ sentinel = "COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT"
469
+ if first.startswith(sentinel):
470
+ lines = lines[1:]
471
+ diff = "\n".join(lines).strip("\n")
472
+ return diff
473
+
474
+ def _evaluate_submission(self, diff: str) -> dict[str, Any]:
475
+ metadata = dict(self.task.get("metadata") or {})
476
+ instance = dict(metadata.get("raw_instance") or {})
477
+ instance_id = instance.setdefault("instance_id", self.task.get("instance_id"))
478
+
479
+ required_fields = ["repo", "base_commit", "test_patch", "version"]
480
+ missing = [field for field in required_fields if not instance.get(field)]
481
+ if missing:
482
+ msg = (
483
+ "Cannot run SWE-bench evaluation; task metadata missing required fields "
484
+ f"{missing}. Ensure the dataset preserves full SWE-bench records."
485
+ )
486
+ logger.error(msg)
487
+ return {"completed": False, "resolved": False, "error": msg}
488
+
489
+ try:
490
+ from swebench.harness.constants import (
491
+ KEY_INSTANCE_ID,
492
+ KEY_MODEL,
493
+ KEY_PREDICTION,
494
+ )
495
+ except Exception as exc: # pragma: no cover - dependency missing
496
+ msg = (
497
+ "SWE-bench harness is required for official scoring. "
498
+ "Install swebench with evaluation extras."
499
+ )
500
+ logger.exception("Failed to import swebench harness constants: %s", exc)
501
+ return {"completed": False, "resolved": False, "error": f"{msg} ({exc})"}
502
+
503
+ backend = self._resolve_evaluation_backend(metadata)
504
+
505
+ image_name = str(metadata.get("image_name") or "")
506
+ namespace = metadata.get("namespace") or self._namespace_from_image(image_name) or "swebench"
507
+ instance_image_tag = metadata.get("instance_image_tag") or self._image_tag_from_name(image_name) or "latest"
508
+ env_image_tag = metadata.get("env_image_tag") or "latest"
509
+
510
+ model_name = metadata.get("submission_model_name") or metadata.get("model_name") or "synth-ai-agent"
511
+ run_id = f"swe_mini_eval_{uuid.uuid4().hex[:12]}"
512
+ eval_timeout = self._resolve_eval_timeout(metadata)
513
+ rm_image = self._to_bool(metadata.get("eval_rm_image") or os.getenv("SWE_MINI_EVAL_RM_IMAGE", "false"))
514
+ force_rebuild = self._to_bool(metadata.get("eval_force_rebuild") or os.getenv("SWE_MINI_EVAL_FORCE_REBUILD", "false"))
515
+
516
+ prediction = {
517
+ KEY_INSTANCE_ID: instance_id,
518
+ KEY_MODEL: model_name,
519
+ KEY_PREDICTION: diff or "",
520
+ }
521
+
522
+ # Ensure log root exists so downstream collection succeeds.
523
+ with contextlib.suppress(Exception):
524
+ from swebench.harness.constants import RUN_EVALUATION_LOG_DIR
525
+
526
+ Path(RUN_EVALUATION_LOG_DIR).mkdir(parents=True, exist_ok=True)
527
+
528
+ if backend == "modal_harness":
529
+ evaluation_payload = self._run_modal_harness(
530
+ instance=instance,
531
+ prediction=prediction,
532
+ run_id=run_id,
533
+ eval_timeout=eval_timeout,
534
+ model_name=model_name,
535
+ )
536
+ elif backend == "swe_rex":
537
+ evaluation_payload = self._run_swe_rex(
538
+ instance=instance,
539
+ prediction=prediction,
540
+ run_id=run_id,
541
+ eval_timeout=eval_timeout,
542
+ namespace=namespace,
543
+ instance_image_tag=instance_image_tag,
544
+ env_image_tag=env_image_tag,
545
+ model_name=model_name,
546
+ )
547
+ else:
548
+ evaluation_payload = self._run_local_harness(
549
+ instance=instance,
550
+ prediction=prediction,
551
+ run_id=run_id,
552
+ eval_timeout=eval_timeout,
553
+ namespace=namespace,
554
+ instance_image_tag=instance_image_tag,
555
+ env_image_tag=env_image_tag,
556
+ rm_image=rm_image,
557
+ force_rebuild=force_rebuild,
558
+ model_name=model_name,
559
+ )
560
+
561
+ evaluation_payload = dict(evaluation_payload or {})
562
+ evaluation_payload.setdefault("backend", backend)
563
+ evaluation_payload.setdefault("run_id", run_id)
564
+ evaluation_payload.setdefault("model_name", model_name)
565
+ evaluation_payload.setdefault("instance_id", instance_id)
566
+
567
+ artifacts = self._collect_evaluation_artifacts(
568
+ run_id=run_id,
569
+ model_name=model_name,
570
+ instance_id=instance_id,
571
+ )
572
+ # Merge artifact data without clobbering explicit error/resolution flags.
573
+ merged = {**artifacts, **evaluation_payload}
574
+ if artifacts.get("completed"):
575
+ merged["completed"] = True
576
+ else:
577
+ merged.setdefault("completed", False)
578
+ if artifacts.get("resolved"):
579
+ merged["resolved"] = True
580
+ else:
581
+ merged.setdefault("resolved", False)
582
+ merged.setdefault("log_dir", artifacts.get("log_dir"))
583
+ merged.setdefault("report_path", artifacts.get("report_path"))
584
+ merged.setdefault("test_output_path", artifacts.get("test_output_path"))
585
+ if artifacts.get("report") and not merged.get("report"):
586
+ merged["report"] = artifacts["report"]
587
+ if artifacts.get("error") and not merged.get("error"):
588
+ merged["error"] = artifacts["error"]
589
+ return merged
590
+
591
+ def _resolve_evaluation_backend(self, metadata: dict[str, Any]) -> str:
592
+ raw = (
593
+ metadata.get("evaluation_backend")
594
+ or self.env_config.get("evaluation_backend")
595
+ or os.getenv("SWE_MINI_EVALUATION_BACKEND")
596
+ or "local"
597
+ )
598
+ backend = str(raw).strip().lower()
599
+ mapping = {
600
+ "": "local",
601
+ "local": "local",
602
+ "docker": "local",
603
+ "modal": "modal_harness",
604
+ "modal_harness": "modal_harness",
605
+ "modal-harness": "modal_harness",
606
+ "modal-harnesses": "modal_harness",
607
+ "swe_rex": "swe_rex",
608
+ "swe-rex": "swe_rex",
609
+ "swerex": "swe_rex",
610
+ }
611
+ return mapping.get(backend, "local")
612
+
613
+ def _resolve_eval_timeout(self, metadata: dict[str, Any]) -> int:
614
+ raw = (
615
+ metadata.get("evaluation_timeout")
616
+ or self.env_config.get("evaluation_timeout")
617
+ or os.getenv("SWE_MINI_EVALUATION_TIMEOUT")
618
+ or 3600
619
+ )
620
+ try:
621
+ value = int(raw)
622
+ except (TypeError, ValueError):
623
+ return 3600
624
+ return max(1, value)
625
+
626
+ def _run_local_harness(
627
+ self,
628
+ *,
629
+ instance: dict[str, Any],
630
+ prediction: dict[str, Any],
631
+ run_id: str,
632
+ eval_timeout: int,
633
+ namespace: str,
634
+ instance_image_tag: str,
635
+ env_image_tag: str,
636
+ rm_image: bool,
637
+ force_rebuild: bool,
638
+ model_name: str,
639
+ ) -> dict[str, Any]:
640
+ try:
641
+ from swebench.harness.run_evaluation import run_instance
642
+ from swebench.harness.test_spec.test_spec import make_test_spec
643
+ except Exception as exc: # pragma: no cover - dependency missing
644
+ msg = (
645
+ "SWE-bench harness is required for official scoring. "
646
+ "Install swebench with evaluation extras."
647
+ )
648
+ logger.exception("Failed to import swebench harness: %s", exc)
649
+ return {"completed": False, "resolved": False, "error": f"{msg} ({exc})", "backend": "local"}
650
+
651
+ try:
652
+ import docker
653
+ except Exception as exc: # pragma: no cover - dependency missing
654
+ msg = "Docker SDK for Python is required to run local SWE-bench evaluation."
655
+ logger.exception("Failed to import docker SDK: %s", exc)
656
+ return {"completed": False, "resolved": False, "error": f"{msg} ({exc})", "backend": "local"}
657
+
658
+ instance_id = str(instance["instance_id"])
659
+ try:
660
+ test_spec = make_test_spec(
661
+ instance,
662
+ namespace=namespace,
663
+ instance_image_tag=instance_image_tag,
664
+ env_image_tag=env_image_tag,
665
+ )
666
+ except Exception as exc:
667
+ logger.exception("Failed to build SWE-bench test spec for %s: %s", instance_id, exc)
668
+ return {"completed": False, "resolved": False, "error": f"Failed to build test spec: {exc}", "backend": "local"}
669
+
670
+ client = None
671
+ result: dict[str, Any] = {}
672
+ try:
673
+ client = docker.from_env()
674
+ result = run_instance(
675
+ test_spec,
676
+ prediction,
677
+ rm_image,
678
+ force_rebuild,
679
+ client,
680
+ run_id,
681
+ int(eval_timeout),
682
+ rewrite_reports=False,
683
+ )
684
+ except Exception as exc:
685
+ logger.exception("Error while running SWE-bench evaluation for %s: %s", instance_id, exc)
686
+ return {"completed": False, "resolved": False, "error": f"Evaluation failed: {exc}", "backend": "local"}
687
+ finally:
688
+ with contextlib.suppress(Exception):
689
+ if client is not None:
690
+ client.close()
691
+
692
+ payload = {
693
+ "completed": bool(result.get("completed")),
694
+ "resolved": bool(result.get("resolved")),
695
+ "backend": "local",
696
+ }
697
+ return payload
698
+
699
+ def _run_modal_harness(
700
+ self,
701
+ *,
702
+ instance: dict[str, Any],
703
+ prediction: dict[str, Any],
704
+ run_id: str,
705
+ eval_timeout: int,
706
+ model_name: str,
707
+ ) -> dict[str, Any]:
708
+ try:
709
+ from swebench.harness.modal_eval import run_instances_modal
710
+ except Exception as exc: # pragma: no cover - dependency missing
711
+ msg = (
712
+ "SWE-bench modal extras are required for the modal_harness backend. "
713
+ "Install swebench[modal] inside the Modal deployment."
714
+ )
715
+ logger.exception("Failed to import swebench modal harness: %s", exc)
716
+ return {"completed": False, "resolved": False, "error": f"{msg} ({exc})", "backend": "modal_harness"}
717
+
718
+ instance_id = str(instance["instance_id"])
719
+ predictions = {instance_id: dict(prediction)}
720
+ dataset = [instance]
721
+ try:
722
+ run_instances_modal(
723
+ predictions,
724
+ dataset,
725
+ dataset,
726
+ run_id,
727
+ int(eval_timeout),
728
+ )
729
+ except Exception as exc:
730
+ logger.exception("Modal SWE-bench evaluation failed for %s: %s", instance_id, exc)
731
+ return {"completed": False, "resolved": False, "error": f"Modal evaluation failed: {exc}", "backend": "modal_harness"}
732
+
733
+ # run_instances_modal writes reports to RUN_EVALUATION_LOG_DIR; we rely on artifact collection.
734
+ return {"backend": "modal_harness"}
735
+
736
+ def _run_swe_rex(
737
+ self,
738
+ *,
739
+ instance: dict[str, Any],
740
+ prediction: dict[str, Any],
741
+ run_id: str,
742
+ eval_timeout: int,
743
+ namespace: str,
744
+ instance_image_tag: str,
745
+ env_image_tag: str,
746
+ model_name: str,
747
+ ) -> dict[str, Any]:
748
+ try:
749
+ from swerex.deployment.config import ModalDeploymentConfig
750
+ from swerex.runtime.abstract import Command, ReadFileRequest, WriteFileRequest
751
+ except ModuleNotFoundError as exc: # pragma: no cover - optional dependency
752
+ msg = (
753
+ "SWE-ReX backend requires the swe-rex package. "
754
+ "Install swe-rex (pip install swe-rex[modal]) to enable this backend."
755
+ )
756
+ logger.exception("Failed to import swe-rex: %s", exc)
757
+ return {"completed": False, "resolved": False, "error": f"{msg} ({exc})", "backend": "swe_rex"}
758
+ except Exception as exc: # pragma: no cover - defensive
759
+ logger.exception("Unexpected swe-rex import failure: %s", exc)
760
+ return {"completed": False, "resolved": False, "error": f"swe-rex import failed: {exc}", "backend": "swe_rex"}
761
+
762
+ image_spec = (
763
+ instance.get("swe_rex_image")
764
+ or self.env_config.get("swe_rex_image")
765
+ or os.getenv("SWE_REX_MODAL_IMAGE")
766
+ or "ghcr.io/swe-agent/swe-rex-modal:latest"
767
+ )
768
+ install_pipx = self._to_bool(
769
+ instance.get("swe_rex_install_pipx")
770
+ or self.env_config.get("swe_rex_install_pipx")
771
+ or os.getenv("SWE_REX_INSTALL_PIPX", "true")
772
+ )
773
+ modal_kwargs_raw = (
774
+ instance.get("swe_rex_modal_kwargs")
775
+ or self.env_config.get("swe_rex_modal_kwargs")
776
+ or os.getenv("SWE_REX_MODAL_SANDBOX_KWARGS")
777
+ )
778
+ modal_kwargs: dict[str, Any] = {}
779
+ if isinstance(modal_kwargs_raw, (dict, list)):
780
+ modal_kwargs = dict(modal_kwargs_raw or {})
781
+ elif isinstance(modal_kwargs_raw, str) and modal_kwargs_raw.strip():
782
+ try:
783
+ modal_kwargs = dict(json.loads(modal_kwargs_raw))
784
+ except Exception as exc: # pragma: no cover - user input parsing
785
+ logger.warning("Failed to parse SWE_REX_MODAL_SANDBOX_KWARGS=%s: %s", modal_kwargs_raw, exc)
786
+
787
+ deployment_config = ModalDeploymentConfig(
788
+ image=image_spec,
789
+ runtime_timeout=float(
790
+ instance.get("swe_rex_runtime_timeout")
791
+ or self.env_config.get("swe_rex_runtime_timeout")
792
+ or os.getenv("SWE_REX_RUNTIME_TIMEOUT", 900)
793
+ ),
794
+ deployment_timeout=float(
795
+ instance.get("swe_rex_deployment_timeout")
796
+ or self.env_config.get("swe_rex_deployment_timeout")
797
+ or os.getenv("SWE_REX_DEPLOYMENT_TIMEOUT", 3600)
798
+ ),
799
+ modal_sandbox_kwargs=modal_kwargs,
800
+ install_pipx=bool(install_pipx),
801
+ )
802
+
803
+ remote_root = (
804
+ instance.get("swe_rex_workdir")
805
+ or self.env_config.get("swe_rex_workdir")
806
+ or os.getenv("SWE_REX_REMOTE_WORKDIR")
807
+ or "/root/swebench_eval"
808
+ )
809
+ remote_root = str(remote_root).rstrip("/")
810
+ dataset_remote_path = f"{remote_root}/dataset.json"
811
+ predictions_remote_path = f"{remote_root}/predictions.json"
812
+
813
+ environment_forward_raw = (
814
+ instance.get("swe_rex_forward_env")
815
+ or self.env_config.get("swe_rex_forward_env")
816
+ or os.getenv("SWE_REX_FORWARD_ENV")
817
+ )
818
+ forward_env: dict[str, str] | None = None
819
+ if isinstance(environment_forward_raw, dict):
820
+ forward_env = {str(k): str(v) for k, v in environment_forward_raw.items()}
821
+ elif isinstance(environment_forward_raw, str) and environment_forward_raw.strip():
822
+ try:
823
+ parsed = json.loads(environment_forward_raw)
824
+ if isinstance(parsed, dict):
825
+ forward_env = {str(k): str(v) for k, v in parsed.items()}
826
+ except Exception as exc: # pragma: no cover - parsing failure
827
+ logger.warning("Failed to parse SWE_REX_FORWARD_ENV=%s: %s", environment_forward_raw, exc)
828
+
829
+ # Build coroutine for the async swe-rex flow.
830
+ coro = self._run_swe_rex_async(
831
+ deployment_config=deployment_config,
832
+ remote_root=remote_root,
833
+ dataset_remote_path=dataset_remote_path,
834
+ predictions_remote_path=predictions_remote_path,
835
+ forward_env=forward_env,
836
+ instance=instance,
837
+ prediction=prediction,
838
+ run_id=run_id,
839
+ eval_timeout=eval_timeout,
840
+ namespace=namespace,
841
+ instance_image_tag=instance_image_tag,
842
+ env_image_tag=env_image_tag,
843
+ model_name=model_name,
844
+ Command=Command,
845
+ WriteFileRequest=WriteFileRequest,
846
+ ReadFileRequest=ReadFileRequest,
847
+ )
848
+ try:
849
+ return self._run_coroutine_blocking(coro)
850
+ except Exception as exc: # pragma: no cover - remote execution failure
851
+ logger.exception("SWE-ReX evaluation failed for %s: %s", instance.get("instance_id"), exc)
852
+ return {"completed": False, "resolved": False, "error": f"SWE-ReX evaluation failed: {exc}", "backend": "swe_rex"}
853
+
854
+ async def _run_swe_rex_async(
855
+ self,
856
+ *,
857
+ deployment_config,
858
+ remote_root: str,
859
+ dataset_remote_path: str,
860
+ predictions_remote_path: str,
861
+ forward_env: dict[str, str] | None,
862
+ instance: dict[str, Any],
863
+ prediction: dict[str, Any],
864
+ run_id: str,
865
+ eval_timeout: int,
866
+ namespace: str,
867
+ instance_image_tag: str,
868
+ env_image_tag: str,
869
+ model_name: str,
870
+ Command,
871
+ WriteFileRequest,
872
+ ReadFileRequest,
873
+ ) -> dict[str, Any]:
874
+ deployment = deployment_config.get_deployment()
875
+ await deployment.start()
876
+ try:
877
+ runtime = deployment.runtime
878
+ instance_id = str(instance["instance_id"])
879
+ safe_model = prediction["model_name_or_path"].replace("/", "__")
880
+
881
+ # Ensure working directory exists.
882
+ mkdir_resp = await runtime.execute(
883
+ Command(command=["mkdir", "-p", remote_root], timeout=60, shell=False)
884
+ )
885
+ if mkdir_resp.exit_code not in (0, None):
886
+ logger.warning("Failed to ensure remote directory %s (exit=%s)", remote_root, mkdir_resp.exit_code)
887
+
888
+ # Upload dataset & predictions.
889
+ dataset_blob = json.dumps([instance], ensure_ascii=False)
890
+ predictions_blob = json.dumps({instance_id: prediction}, ensure_ascii=False)
891
+ await runtime.write_file(WriteFileRequest(path=dataset_remote_path, content=dataset_blob))
892
+ await runtime.write_file(WriteFileRequest(path=predictions_remote_path, content=predictions_blob))
893
+
894
+ eval_cmd = [
895
+ "python",
896
+ "-m",
897
+ "swebench.harness.run_evaluation",
898
+ "--dataset_name",
899
+ dataset_remote_path,
900
+ "--split",
901
+ "test",
902
+ "--instance_ids",
903
+ instance_id,
904
+ "--predictions_path",
905
+ predictions_remote_path,
906
+ "-id",
907
+ run_id,
908
+ "--modal",
909
+ "true",
910
+ "--timeout",
911
+ str(eval_timeout),
912
+ "--namespace",
913
+ namespace,
914
+ "--instance_image_tag",
915
+ instance_image_tag,
916
+ "--env_image_tag",
917
+ env_image_tag,
918
+ "--max_workers",
919
+ "1",
920
+ ]
921
+
922
+ command_timeout = max(eval_timeout + 900, 1200)
923
+ response = await runtime.execute(
924
+ Command(
925
+ command=eval_cmd,
926
+ timeout=command_timeout,
927
+ cwd=remote_root,
928
+ env=forward_env,
929
+ shell=False,
930
+ merge_output_streams=True,
931
+ )
932
+ )
933
+ command_output = (response.stdout or "") + (response.stderr or "")
934
+ exit_code = response.exit_code if response.exit_code is not None else -1
935
+
936
+ # Retrieve artifacts back to local disk.
937
+ artifacts = {}
938
+ try:
939
+ from swebench.harness.constants import RUN_EVALUATION_LOG_DIR
940
+
941
+ local_log_dir = Path(RUN_EVALUATION_LOG_DIR) / run_id / safe_model / instance_id
942
+ local_log_dir.mkdir(parents=True, exist_ok=True)
943
+
944
+ remote_log_dir = f"{remote_root}/logs/run_evaluation/{run_id}/{safe_model}/{instance_id}"
945
+ for filename in ("report.json", "test_output.txt", "run_instance.log", "patch.diff"):
946
+ remote_path = f"{remote_log_dir}/{filename}"
947
+ try:
948
+ content = await runtime.read_file(ReadFileRequest(path=remote_path))
949
+ except Exception:
950
+ continue
951
+ if getattr(content, "content", None):
952
+ (local_log_dir / filename).write_text(content.content)
953
+
954
+ artifacts = {
955
+ "log_dir": str(local_log_dir),
956
+ }
957
+ except Exception as exc: # pragma: no cover - best effort artifact copy
958
+ logger.warning("Failed to copy SWE-ReX artifacts locally: %s", exc)
959
+
960
+ payload = {
961
+ "backend": "swe_rex",
962
+ "command_exit_code": exit_code,
963
+ "command_output": command_output[-4000:] if command_output else "",
964
+ "artifacts": artifacts,
965
+ }
966
+ if exit_code == 0:
967
+ payload.setdefault("completed", True)
968
+ return payload
969
+ finally:
970
+ with contextlib.suppress(Exception):
971
+ await deployment.stop()
972
+
973
+ def _collect_evaluation_artifacts(
974
+ self,
975
+ *,
976
+ run_id: str,
977
+ model_name: str,
978
+ instance_id: str,
979
+ ) -> dict[str, Any]:
980
+ try:
981
+ from swebench.harness.constants import (
982
+ LOG_REPORT,
983
+ LOG_TEST_OUTPUT,
984
+ RUN_EVALUATION_LOG_DIR,
985
+ )
986
+ except Exception: # pragma: no cover - dependency missing
987
+ return {
988
+ "completed": False,
989
+ "resolved": False,
990
+ "log_dir": None,
991
+ "report_path": None,
992
+ "test_output_path": None,
993
+ }
994
+
995
+ log_model = model_name.replace("/", "__")
996
+ log_dir = Path(RUN_EVALUATION_LOG_DIR) / run_id / log_model / instance_id
997
+ payload: dict[str, Any] = {
998
+ "log_dir": str(log_dir),
999
+ "report_path": None,
1000
+ "test_output_path": None,
1001
+ "report": None,
1002
+ "completed": False,
1003
+ "resolved": False,
1004
+ }
1005
+
1006
+ if not log_dir.exists():
1007
+ return payload
1008
+
1009
+ report_path = log_dir / LOG_REPORT
1010
+ if report_path.exists():
1011
+ payload["report_path"] = str(report_path)
1012
+ try:
1013
+ report_blob = json.loads(report_path.read_text())
1014
+ per_instance = report_blob.get(instance_id)
1015
+ if per_instance is not None:
1016
+ payload["report"] = per_instance
1017
+ payload["completed"] = True
1018
+ payload["resolved"] = bool(per_instance.get("resolved"))
1019
+ except Exception as exc: # pragma: no cover - log parsing failure
1020
+ logger.exception("Failed to parse SWE-bench report for %s: %s", instance_id, exc)
1021
+ payload["error"] = f"Failed to parse report.json: {exc}"
1022
+
1023
+ test_output_path = log_dir / LOG_TEST_OUTPUT
1024
+ if test_output_path.exists():
1025
+ payload["test_output_path"] = str(test_output_path)
1026
+
1027
+ return payload
1028
+
1029
+ @staticmethod
1030
+ def _run_coroutine_blocking(coro):
1031
+ try:
1032
+ loop = asyncio.get_running_loop()
1033
+ except RuntimeError:
1034
+ loop = None
1035
+
1036
+ if loop and loop.is_running():
1037
+ result: dict[str, Any] = {}
1038
+ error: dict[str, Exception] = {}
1039
+
1040
+ def runner():
1041
+ try:
1042
+ result["value"] = asyncio.run(coro)
1043
+ except Exception as exc: # pragma: no cover - propagate to caller
1044
+ error["exc"] = exc
1045
+
1046
+ thread = threading.Thread(target=runner, daemon=True)
1047
+ thread.start()
1048
+ thread.join()
1049
+ if error:
1050
+ raise error["exc"]
1051
+ return result.get("value")
1052
+
1053
+ return asyncio.run(coro)
1054
+
1055
+ @staticmethod
1056
+ def _namespace_from_image(image_name: str) -> str | None:
1057
+ if not image_name:
1058
+ return None
1059
+ parts = image_name.split("/")
1060
+ if len(parts) >= 2:
1061
+ return parts[-2] if parts[0].endswith(".io") else parts[0]
1062
+ return None
1063
+
1064
+ @staticmethod
1065
+ def _image_tag_from_name(image_name: str) -> str | None:
1066
+ if not image_name or ":" not in image_name:
1067
+ return None
1068
+ return image_name.rsplit(":", 1)[-1] or None
1069
+
1070
+ @staticmethod
1071
+ def _to_bool(value: Any) -> bool:
1072
+ if isinstance(value, bool):
1073
+ return value
1074
+ if isinstance(value, str):
1075
+ return value.strip().lower() in {"1", "true", "yes", "on"}
1076
+ if isinstance(value, (int, float)):
1077
+ return bool(value)
1078
+ return False # pragma: no cover - defensive default
1079
+
1080
+ def _build_observation(self, last_result: dict[str, Any] | None) -> dict[str, Any]:
1081
+ trimmed_history = summarise_history(self.state.history)
1082
+ observation = {
1083
+ "task": self.task,
1084
+ "step_idx": self.state.step_idx,
1085
+ "history": trimmed_history,
1086
+ "submitted": self.state.submitted,
1087
+ "submission_success": self.state.submission_success,
1088
+ "tools": TOOLS_SCHEMA,
1089
+ }
1090
+ if last_result is not None:
1091
+ observation["last"] = last_result
1092
+ if self.last_submission is not None:
1093
+ observation["submission_result"] = self.last_submission
1094
+ return observation
1095
+
1096
+ def _build_response(
1097
+ self,
1098
+ *,
1099
+ observation: dict[str, Any],
1100
+ step_idx: int,
1101
+ done: bool = False,
1102
+ reward: float | None = None,
1103
+ info: dict[str, Any] | None = None,
1104
+ ) -> dict[str, Any]:
1105
+ response = {
1106
+ "observation": observation,
1107
+ "step_idx": step_idx,
1108
+ "done": bool(done),
1109
+ }
1110
+ if reward is not None:
1111
+ response["reward"] = reward
1112
+ if info is not None:
1113
+ response["info"] = info
1114
+ return response
1115
+
1116
+ def state_dict(self) -> dict[str, Any]:
1117
+ return {
1118
+ "task": self.state.task,
1119
+ "history": self.state.history,
1120
+ "step_idx": self.state.step_idx,
1121
+ "submitted": self.state.submitted,
1122
+ "submission_success": self.state.submission_success,
1123
+ "last_result": self.last_result,
1124
+ "last_submission": self.last_submission,
1125
+ "environment_type": self.environment_type,
1126
+ "env_config": self.env_config,
1127
+ }
1128
+
1129
+ def load_state_dict(self, payload: dict[str, Any]) -> None:
1130
+ self.state = MiniSweEnvironmentState(
1131
+ task=payload["task"],
1132
+ history=payload.get("history", []),
1133
+ step_idx=int(payload.get("step_idx", 0)),
1134
+ submitted=bool(payload.get("submitted", False)),
1135
+ submission_success=payload.get("submission_success"),
1136
+ )
1137
+ self.last_result = payload.get("last_result")
1138
+ self.last_submission = payload.get("last_submission")
1139
+ self.environment_type = payload.get("environment_type", self.environment_type)
1140
+ self.env_config = payload.get("env_config", self.env_config)
1141
+
1142
+ async def serialize(self) -> dict[str, Any]:
1143
+ return {
1144
+ "name": self.name,
1145
+ "config": {
1146
+ "env_config": self.env_config,
1147
+ "submit_command": self.submit_command,
1148
+ },
1149
+ "state": self.state_dict(),
1150
+ }
1151
+
1152
+ @classmethod
1153
+ async def deserialize(cls, payload: dict[str, Any]) -> MiniSweEnvironmentWrapper:
1154
+ config = payload.get("config", {}) or {}
1155
+ wrapper = cls(
1156
+ task=payload["state"]["task"],
1157
+ env_config=config.get("env_config"),
1158
+ submit_command=config.get("submit_command"),
1159
+ )
1160
+ wrapper.load_state_dict(payload["state"])
1161
+ return wrapper
1162
+
1163
+
1164
+ __all__ = ["MiniSweEnvironmentWrapper"]