synth-ai 0.2.8.dev4__py3-none-any.whl → 0.2.23.dev3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (889) hide show
  1. examples/README.md +1 -0
  2. examples/__init__.py +16 -0
  3. examples/analyze_semantic_words.sh +17 -0
  4. examples/baseline/banking77_baseline.py +243 -0
  5. examples/baseline/banking77_pipeline_baseline.py +294 -0
  6. examples/baseline/crafter_baseline.py +407 -0
  7. examples/baseline/pokemon_red_baseline.py +326 -0
  8. examples/baseline/simple_baseline.py +56 -0
  9. examples/baseline/warming_up_to_rl_baseline.py +239 -0
  10. examples/blog_posts/gepa/README.md +355 -0
  11. examples/blog_posts/gepa/configs/banking77_gepa_local.toml +95 -0
  12. examples/blog_posts/gepa/configs/banking77_gepa_test.toml +80 -0
  13. examples/blog_posts/gepa/configs/banking77_mipro_local.toml +50 -0
  14. examples/blog_posts/gepa/configs/banking77_pipeline_gepa_local.toml +101 -0
  15. examples/blog_posts/gepa/configs/banking77_pipeline_gepa_test.toml +96 -0
  16. examples/blog_posts/gepa/configs/hotpotqa_gepa_local.toml +57 -0
  17. examples/blog_posts/gepa/configs/hotpotqa_gepa_qwen.toml +35 -0
  18. examples/blog_posts/gepa/configs/hotpotqa_mipro_local.toml +51 -0
  19. examples/blog_posts/gepa/configs/hover_gepa_local.toml +57 -0
  20. examples/blog_posts/gepa/configs/hover_gepa_qwen.toml +35 -0
  21. examples/blog_posts/gepa/configs/hover_mipro_local.toml +51 -0
  22. examples/blog_posts/gepa/configs/ifbench_gepa_local.toml +57 -0
  23. examples/blog_posts/gepa/configs/ifbench_gepa_qwen.toml +35 -0
  24. examples/blog_posts/gepa/configs/ifbench_mipro_local.toml +51 -0
  25. examples/blog_posts/gepa/configs/pupa_gepa_local.toml +58 -0
  26. examples/blog_posts/gepa/configs/pupa_mipro_local.toml +52 -0
  27. examples/blog_posts/gepa/deploy_banking77_task_app.sh +54 -0
  28. examples/blog_posts/gepa/gepa_baseline.py +204 -0
  29. examples/blog_posts/gepa/query_prompts_example.py +97 -0
  30. examples/blog_posts/gepa/run_gepa_banking77.sh +112 -0
  31. examples/blog_posts/gepa/run_gepa_banking77_pipeline.sh +163 -0
  32. examples/blog_posts/gepa/task_apps.py +105 -0
  33. examples/blog_posts/gepa/test_gepa_local.sh +67 -0
  34. examples/blog_posts/gepa/verify_banking77_setup.sh +123 -0
  35. examples/blog_posts/mipro/README.md +415 -0
  36. examples/blog_posts/mipro/configs/banking77_mipro_local.toml +91 -0
  37. examples/blog_posts/mipro/configs/banking77_mipro_test.toml +87 -0
  38. examples/blog_posts/mipro/configs/banking77_pipeline_mipro_gemini_flash_lite_local.toml +98 -0
  39. examples/blog_posts/mipro/configs/banking77_pipeline_mipro_gpt41mini_local.toml +96 -0
  40. examples/blog_posts/mipro/configs/banking77_pipeline_mipro_local.toml +94 -0
  41. examples/blog_posts/mipro/configs/banking77_pipeline_mipro_test.toml +170 -0
  42. examples/blog_posts/mipro/deploy_banking77_pipeline_task_app.sh +59 -0
  43. examples/blog_posts/mipro/deploy_banking77_task_app.sh +41 -0
  44. examples/blog_posts/mipro/multi_step.md +79 -0
  45. examples/blog_posts/mipro/run_mipro_banking77.sh +191 -0
  46. examples/blog_posts/mipro/run_mipro_banking77_pipeline.sh +171 -0
  47. examples/blog_posts/mipro/run_mipro_banking77_pipeline_gemini_flash_lite.sh +177 -0
  48. examples/blog_posts/mipro/run_mipro_banking77_pipeline_gpt41mini.sh +173 -0
  49. examples/blog_posts/mipro/verify_banking77_setup.sh +117 -0
  50. examples/blog_posts/pokemon_vl/README.md +98 -0
  51. examples/blog_posts/pokemon_vl/configs/eval_gpt5nano.toml +26 -0
  52. examples/blog_posts/pokemon_vl/configs/eval_qwen3_vl.toml +27 -0
  53. examples/blog_posts/pokemon_vl/configs/eval_rl_final.toml +24 -0
  54. examples/blog_posts/pokemon_vl/configs/filter_high_reward.toml +10 -0
  55. examples/blog_posts/pokemon_vl/configs/train_rl_from_sft.toml +43 -0
  56. examples/blog_posts/pokemon_vl/configs/train_sft_qwen4b_vl.toml +40 -0
  57. examples/blog_posts/pokemon_vl/extract_images.py +239 -0
  58. examples/blog_posts/pokemon_vl/pokemon_vl_baseline.py +326 -0
  59. examples/blog_posts/pokemon_vl/run_eval_extract_images.py +209 -0
  60. examples/blog_posts/pokemon_vl/run_qwen_eval_extract_images.py +212 -0
  61. examples/blog_posts/pokemon_vl/text_box_analysis.md +106 -0
  62. examples/blog_posts/warming_up_to_rl/ARCHITECTURE.md +195 -0
  63. examples/blog_posts/warming_up_to_rl/FINAL_TEST_RESULTS.md +127 -0
  64. examples/blog_posts/warming_up_to_rl/INFERENCE_SUCCESS.md +132 -0
  65. examples/blog_posts/warming_up_to_rl/README.md +158 -0
  66. examples/blog_posts/warming_up_to_rl/SMOKE_TESTING.md +164 -0
  67. examples/blog_posts/warming_up_to_rl/SMOKE_TEST_COMPLETE.md +253 -0
  68. examples/blog_posts/warming_up_to_rl/configs/eval_baseline_qwen32b_10x20.toml +25 -0
  69. examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b.toml +25 -0
  70. examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b_10x20.toml +26 -0
  71. examples/blog_posts/warming_up_to_rl/configs/eval_groq_qwen32b.toml +25 -0
  72. examples/blog_posts/warming_up_to_rl/configs/eval_openai_gpt_oss_120b.toml +29 -0
  73. examples/blog_posts/warming_up_to_rl/configs/filter_high_reward_dataset.toml +10 -0
  74. examples/blog_posts/warming_up_to_rl/configs/smoke_test.toml +75 -0
  75. examples/blog_posts/warming_up_to_rl/configs/train_rl_from_sft.toml +91 -0
  76. examples/blog_posts/warming_up_to_rl/configs/train_sft_qwen4b.toml +40 -0
  77. examples/blog_posts/warming_up_to_rl/warming_up_to_rl_baseline.py +187 -0
  78. examples/crafter_debug_render.py +186 -0
  79. examples/dev/qwen3_32b_qlora_4xh100.toml +45 -0
  80. examples/gepa/banking77_pipeline_gepa.toml +96 -0
  81. examples/gepa/multi_stage_gepa_example.toml +84 -0
  82. examples/gepa/run_gepa_banking77_pipeline.sh +157 -0
  83. examples/multi_step/SFT_README.md +147 -0
  84. examples/multi_step/configs/README_verilog_rl.md +77 -0
  85. examples/multi_step/configs/VERILOG_REWARDS.md +103 -0
  86. examples/multi_step/configs/VERILOG_RL_CHECKLIST.md +196 -0
  87. examples/multi_step/configs/crafter_eval_synth_qwen4b.toml +35 -0
  88. examples/multi_step/configs/crafter_eval_text_only_groq_qwen32b.toml +36 -0
  89. examples/multi_step/configs/crafter_rl_outcome.toml +75 -0
  90. examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +145 -0
  91. examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +84 -0
  92. examples/multi_step/configs/crafter_rl_stepwise_simple.toml +79 -0
  93. examples/multi_step/configs/crafter_rl_stepwise_simple_NEW_FORMAT.toml +105 -0
  94. examples/multi_step/configs/crafter_sft_qwen30b_lora.toml +62 -0
  95. examples/multi_step/configs/crafter_synth_backend.md +40 -0
  96. examples/multi_step/configs/verilog_eval_groq_qwen32b.toml +31 -0
  97. examples/multi_step/configs/verilog_eval_synth_qwen8b.toml +33 -0
  98. examples/multi_step/configs/verilog_rl_lora.toml +147 -0
  99. examples/multi_step/convert_traces_to_sft.py +84 -0
  100. examples/multi_step/crafter_rl_lora.md +70 -0
  101. examples/multi_step/judges/crafter_backend_judge.py +220 -0
  102. examples/multi_step/judges/verilog_backend_judge.py +234 -0
  103. examples/multi_step/readme.md +48 -0
  104. examples/multi_step/run_sft_qwen30b.sh +45 -0
  105. examples/multi_step/sse_metrics_streaming_notes.md +357 -0
  106. examples/multi_step/task_app_config_notes.md +494 -0
  107. examples/multi_step/verilog_rl_lora.md +218 -0
  108. examples/qwen_coder/README.md +102 -0
  109. examples/qwen_coder/_shared.py +113 -0
  110. examples/qwen_coder/configs/coder_lora_30b.toml +60 -0
  111. examples/qwen_coder/configs/coder_lora_4b.toml +61 -0
  112. examples/qwen_coder/configs/coder_lora_small.toml +57 -0
  113. examples/qwen_coder/generate_dataset.py +98 -0
  114. examples/qwen_coder/infer_ft_smoke.py +65 -0
  115. examples/qwen_coder/infer_prod_proxy.py +73 -0
  116. examples/qwen_coder/infer_via_synth.py +87 -0
  117. examples/qwen_coder/scripts/infer_coder.sh +19 -0
  118. examples/qwen_coder/scripts/train_coder_30b.sh +22 -0
  119. examples/qwen_coder/sft_full_17b.py +103 -0
  120. examples/qwen_coder/sft_lora_30b.py +110 -0
  121. examples/qwen_coder/subset_jsonl.py +39 -0
  122. examples/qwen_coder/todos.md +38 -0
  123. examples/qwen_coder/validate_jsonl.py +60 -0
  124. examples/qwen_vl/BUGS_AND_FIXES.md +232 -0
  125. examples/qwen_vl/IMAGE_VALIDATION_COMPLETE.md +271 -0
  126. examples/qwen_vl/IMAGE_VALIDATION_SUMMARY.md +260 -0
  127. examples/qwen_vl/INFERENCE_SFT_TESTS.md +412 -0
  128. examples/qwen_vl/NEXT_STEPS_2B.md +325 -0
  129. examples/qwen_vl/QUICKSTART.md +327 -0
  130. examples/qwen_vl/QUICKSTART_RL_VISION.md +110 -0
  131. examples/qwen_vl/README.md +152 -0
  132. examples/qwen_vl/RL_VISION_COMPLETE.md +475 -0
  133. examples/qwen_vl/RL_VISION_TESTING.md +333 -0
  134. examples/qwen_vl/SDK_VISION_INTEGRATION.md +328 -0
  135. examples/qwen_vl/SETUP_COMPLETE.md +274 -0
  136. examples/qwen_vl/VISION_TESTS_COMPLETE.md +489 -0
  137. examples/qwen_vl/VLM_PIPELINE_COMPLETE.md +242 -0
  138. examples/qwen_vl/__init__.py +2 -0
  139. examples/qwen_vl/collect_data_via_cli.md +415 -0
  140. examples/qwen_vl/collect_vision_traces.py +368 -0
  141. examples/qwen_vl/configs/crafter_rl_vision_qwen3vl4b.toml +110 -0
  142. examples/qwen_vl/configs/crafter_vlm_sft_example.toml +59 -0
  143. examples/qwen_vl/configs/eval_gpt4o_mini_vision.toml +26 -0
  144. examples/qwen_vl/configs/eval_gpt4o_vision_proper.toml +29 -0
  145. examples/qwen_vl/configs/eval_gpt5nano_vision.toml +26 -0
  146. examples/qwen_vl/configs/eval_qwen3vl_vision.toml +26 -0
  147. examples/qwen_vl/configs/filter_qwen3vl_sft.toml +49 -0
  148. examples/qwen_vl/configs/filter_vision_sft.toml +52 -0
  149. examples/qwen_vl/configs/filter_vision_test.toml +8 -0
  150. examples/qwen_vl/configs/sft_qwen3_vl_2b_test.toml +54 -0
  151. examples/qwen_vl/crafter_gpt5nano_agent.py +308 -0
  152. examples/qwen_vl/crafter_qwen_vl_agent.py +300 -0
  153. examples/qwen_vl/run_vision_comparison.sh +61 -0
  154. examples/qwen_vl/run_vision_sft_pipeline.sh +175 -0
  155. examples/qwen_vl/test_image_validation.py +201 -0
  156. examples/qwen_vl/test_sft_vision_data.py +110 -0
  157. examples/rl/README.md +169 -0
  158. examples/rl/configs/eval_base_qwen.toml +17 -0
  159. examples/rl/configs/eval_rl_qwen.toml +13 -0
  160. examples/rl/configs/rl_from_base_qwen.toml +62 -0
  161. examples/rl/configs/rl_from_base_qwen17.toml +80 -0
  162. examples/rl/configs/rl_from_ft_qwen.toml +37 -0
  163. examples/rl/download_dataset.py +80 -0
  164. examples/rl/run_eval.py +436 -0
  165. examples/rl/run_rl_and_save.py +111 -0
  166. examples/rl/task_app/README.md +21 -0
  167. examples/rl/task_app/math_single_step.py +990 -0
  168. examples/rl/task_app/math_task_app.py +111 -0
  169. examples/run_crafter_demo.sh +10 -0
  170. examples/sdk_prompt_learning_example.py +55 -0
  171. examples/sft/README.md +139 -0
  172. examples/sft/configs/crafter_fft_qwen0p6b.toml +49 -0
  173. examples/sft/configs/crafter_lora_qwen0p6b.toml +49 -0
  174. examples/sft/evaluate.py +117 -0
  175. examples/sft/export_dataset.py +120 -0
  176. examples/sft/generate_traces.py +164 -0
  177. examples/swe/__init__.py +12 -0
  178. examples/swe/task_app/README.md +135 -0
  179. examples/swe/task_app/__init__.py +2 -0
  180. examples/swe/task_app/grpo_swe_mini.py +604 -0
  181. examples/swe/task_app/grpo_swe_mini_task_app.py +124 -0
  182. examples/swe/task_app/hosted/README.md +173 -0
  183. examples/swe/task_app/hosted/__init__.py +5 -0
  184. examples/swe/task_app/hosted/branching.py +143 -0
  185. examples/swe/task_app/hosted/environment_routes.py +1289 -0
  186. examples/swe/task_app/hosted/envs/__init__.py +1 -0
  187. examples/swe/task_app/hosted/envs/crafter/__init__.py +6 -0
  188. examples/swe/task_app/hosted/envs/crafter/app.py +1 -0
  189. examples/swe/task_app/hosted/envs/crafter/environment.py +522 -0
  190. examples/swe/task_app/hosted/envs/crafter/policy.py +478 -0
  191. examples/swe/task_app/hosted/envs/crafter/react_agent.py +108 -0
  192. examples/swe/task_app/hosted/envs/crafter/shared.py +305 -0
  193. examples/swe/task_app/hosted/envs/crafter/tools.py +47 -0
  194. examples/swe/task_app/hosted/envs/mini_swe/__init__.py +8 -0
  195. examples/swe/task_app/hosted/envs/mini_swe/environment.py +1191 -0
  196. examples/swe/task_app/hosted/envs/mini_swe/policy.py +355 -0
  197. examples/swe/task_app/hosted/envs/mini_swe/shared.py +83 -0
  198. examples/swe/task_app/hosted/envs/mini_swe/tools.py +96 -0
  199. examples/swe/task_app/hosted/hosted_app.py +204 -0
  200. examples/swe/task_app/hosted/inference/__init__.py +5 -0
  201. examples/swe/task_app/hosted/inference/openai_client.py +584 -0
  202. examples/swe/task_app/hosted/main.py +100 -0
  203. examples/swe/task_app/hosted/policy_routes.py +1094 -0
  204. examples/swe/task_app/hosted/registry.py +195 -0
  205. examples/swe/task_app/hosted/rollout.py +1905 -0
  206. examples/swe/task_app/hosted/storage/__init__.py +5 -0
  207. examples/swe/task_app/hosted/storage/volume.py +211 -0
  208. examples/swe/task_app/hosted/test_agents.py +161 -0
  209. examples/swe/task_app/hosted/test_service.py +136 -0
  210. examples/swe/task_app/hosted/utils.py +62 -0
  211. examples/swe/task_app/morph_backend.py +178 -0
  212. examples/task_apps/IMAGE_ONLY_EVAL_QUICKSTART.md +258 -0
  213. examples/task_apps/TESTING.md +275 -0
  214. examples/task_apps/banking77/__init__.py +6 -0
  215. examples/task_apps/banking77/banking77_task_app.py +912 -0
  216. examples/task_apps/banking77/deploy_wrapper.py +46 -0
  217. examples/task_apps/banking77_pipeline/__init__.py +6 -0
  218. examples/task_apps/banking77_pipeline/banking77_pipeline_task_app.py +489 -0
  219. examples/task_apps/banking77_pipeline/deploy_wrapper.py +50 -0
  220. examples/task_apps/crafter/CREATE_SFT_DATASET.md +286 -0
  221. examples/task_apps/crafter/EVAL_IMAGE_ONLY_RESULTS.md +152 -0
  222. examples/task_apps/crafter/FILTER_COMMAND_STATUS.md +187 -0
  223. examples/task_apps/crafter/FILTER_COMMAND_SUCCESS.md +281 -0
  224. examples/task_apps/crafter/QUERY_EXAMPLES.md +203 -0
  225. examples/task_apps/crafter/README_IMAGE_ONLY_EVAL.md +316 -0
  226. examples/task_apps/crafter/eval_image_only_gpt4o.toml +28 -0
  227. examples/task_apps/crafter/eval_text_only_groq_llama.toml +36 -0
  228. examples/task_apps/crafter/filter_sft_dataset.toml +16 -0
  229. examples/task_apps/crafter/task_app/README.md +42 -0
  230. examples/task_apps/crafter/task_app/__init__.py +5 -0
  231. examples/task_apps/crafter/task_app/grpo_crafter.py +1055 -0
  232. examples/task_apps/crafter/task_app/grpo_crafter_task_app.py +146 -0
  233. examples/task_apps/crafter/task_app/synth_envs_hosted/README.md +173 -0
  234. examples/task_apps/crafter/task_app/synth_envs_hosted/__init__.py +5 -0
  235. examples/task_apps/crafter/task_app/synth_envs_hosted/branching.py +143 -0
  236. examples/task_apps/crafter/task_app/synth_envs_hosted/environment_routes.py +1226 -0
  237. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/__init__.py +1 -0
  238. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/__init__.py +6 -0
  239. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/app.py +1 -0
  240. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/environment.py +532 -0
  241. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +583 -0
  242. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/react_agent.py +122 -0
  243. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/shared.py +305 -0
  244. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/tools.py +47 -0
  245. examples/task_apps/crafter/task_app/synth_envs_hosted/hosted_app.py +253 -0
  246. examples/task_apps/crafter/task_app/synth_envs_hosted/inference/__init__.py +5 -0
  247. examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +999 -0
  248. examples/task_apps/crafter/task_app/synth_envs_hosted/main.py +100 -0
  249. examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +1252 -0
  250. examples/task_apps/crafter/task_app/synth_envs_hosted/registry.py +195 -0
  251. examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +2233 -0
  252. examples/task_apps/crafter/task_app/synth_envs_hosted/storage/__init__.py +5 -0
  253. examples/task_apps/crafter/task_app/synth_envs_hosted/storage/volume.py +211 -0
  254. examples/task_apps/crafter/task_app/synth_envs_hosted/test_agents.py +161 -0
  255. examples/task_apps/crafter/task_app/synth_envs_hosted/test_service.py +136 -0
  256. examples/task_apps/crafter/task_app/synth_envs_hosted/utils.py +411 -0
  257. examples/task_apps/dev/pokemon_emerald/__init__.py +2 -0
  258. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/README.md +811 -0
  259. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/__init__.py +120 -0
  260. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/action.py +160 -0
  261. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/memory.py +155 -0
  262. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/perception.py +69 -0
  263. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/planning.py +96 -0
  264. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/simple.py +1502 -0
  265. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/system_prompt.py +4 -0
  266. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/grab_map.py +68 -0
  267. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/manual.py +216 -0
  268. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/__init__.py +35 -0
  269. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/emerald_utils.py +631 -0
  270. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/emulator.py +1544 -0
  271. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/enums.py +1428 -0
  272. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/memory_reader.py +4848 -0
  273. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/types.py +41 -0
  274. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/utils.py +298 -0
  275. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pyproject.toml +95 -0
  276. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/run.py +204 -0
  277. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/app.py +2152 -0
  278. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/client.py +429 -0
  279. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/frame_server.py +155 -0
  280. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/README.md +78 -0
  281. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/run_tests.py +122 -0
  282. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_agent_direct.py +76 -0
  283. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_agent_prompts.py +413 -0
  284. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_battle_state_formatting.py +204 -0
  285. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_dialogue_detection.py +133 -0
  286. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_dialogue_detection_comprehensive.py +229 -0
  287. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_direct_agent_emulator.py +300 -0
  288. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_fps_adjustment_pytest.py +205 -0
  289. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_house_to_outside_direct.py +200 -0
  290. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_house_to_outside_transition.py +284 -0
  291. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_map_ground_truth_comparison.py +468 -0
  292. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_memory_map.py +575 -0
  293. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_server_map_validation.py +311 -0
  294. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_torchic_state.py +259 -0
  295. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/anticheat.py +372 -0
  296. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/checkpoint.py +296 -0
  297. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/error_handler.py +275 -0
  298. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/get_local_ip.py +22 -0
  299. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/helpers.py +44 -0
  300. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/llm_logger.py +514 -0
  301. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_formatter.py +415 -0
  302. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_stitcher.py +1763 -0
  303. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_stitcher_singleton.py +33 -0
  304. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_trimmer.py +106 -0
  305. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_visualizer.py +334 -0
  306. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/ocr_dialogue.py +1020 -0
  307. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/recording.py +188 -0
  308. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/state_formatter.py +1481 -0
  309. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/vlm.py +862 -0
  310. examples/task_apps/dev/pokemon_emerald/modal_app.py +114 -0
  311. examples/task_apps/dev/pokemon_emerald/task_app/README.md +81 -0
  312. examples/task_apps/dev/pokemon_emerald/task_app/__init__.py +6 -0
  313. examples/task_apps/dev/pokemon_emerald/task_app/pokemon_emerald.py +685 -0
  314. examples/task_apps/enron/__init__.py +2 -0
  315. examples/task_apps/enron/eval_groq_qwen32.toml +16 -0
  316. examples/task_apps/enron/filter_sft.toml +5 -0
  317. examples/task_apps/enron/task_app/README.md +14 -0
  318. examples/task_apps/enron/task_app/__init__.py +1 -0
  319. examples/task_apps/enron/task_app/grpo_enron.py +906 -0
  320. examples/task_apps/enron/task_app/grpo_enron_task_app.py +146 -0
  321. examples/task_apps/enron/tests/__init__.py +4 -0
  322. examples/task_apps/enron/tests/conftest.py +115 -0
  323. examples/task_apps/enron/tests/integration/__init__.py +4 -0
  324. examples/task_apps/enron/tests/integration/test_enron_eval.py +179 -0
  325. examples/task_apps/enron/tests/integration/test_enron_rollout.py +135 -0
  326. examples/task_apps/enron/tests/unit/__init__.py +4 -0
  327. examples/task_apps/enron/tests/unit/test_enron_environment.py +126 -0
  328. examples/task_apps/gepa_benchmarks/__init__.py +7 -0
  329. examples/task_apps/gepa_benchmarks/common.py +260 -0
  330. examples/task_apps/gepa_benchmarks/hotpotqa_task_app.py +507 -0
  331. examples/task_apps/gepa_benchmarks/hover_task_app.py +436 -0
  332. examples/task_apps/gepa_benchmarks/ifbench_task_app.py +563 -0
  333. examples/task_apps/gepa_benchmarks/pupa_task_app.py +460 -0
  334. examples/task_apps/math/README.md +21 -0
  335. examples/task_apps/math/math_single_step.py +1000 -0
  336. examples/task_apps/math/math_task_app.py +115 -0
  337. examples/task_apps/pokemon_battle/__init__.py +2 -0
  338. examples/task_apps/pokemon_battle/modal_app.py +104 -0
  339. examples/task_apps/pokemon_battle/task_app/README.md +68 -0
  340. examples/task_apps/pokemon_battle/task_app/__init__.py +6 -0
  341. examples/task_apps/pokemon_battle/task_app/pokemon_showdown.py +932 -0
  342. examples/task_apps/pokemon_red/EVAL_IMAGE_ONLY_COMPLETE.md +283 -0
  343. examples/task_apps/pokemon_red/EVAL_IMAGE_ONLY_STATUS.md +155 -0
  344. examples/task_apps/pokemon_red/README.md +356 -0
  345. examples/task_apps/pokemon_red/README_IMAGE_ONLY_EVAL.md +428 -0
  346. examples/task_apps/pokemon_red/__init__.py +3 -0
  347. examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +30 -0
  348. examples/task_apps/pokemon_red/eval_pokemon_red_policy.py +224 -0
  349. examples/task_apps/pokemon_red/pallet_town_rl_config.toml +75 -0
  350. examples/task_apps/pokemon_red/task_app.py +1048 -0
  351. examples/task_apps/pokemon_red/test_pallet_town_rewards.py +193 -0
  352. examples/task_apps/sokoban/README.md +306 -0
  353. examples/task_apps/sokoban/__init__.py +3 -0
  354. examples/task_apps/sokoban/eval_groq_qwen32.toml +16 -0
  355. examples/task_apps/sokoban/eval_openai_gpt5.toml +16 -0
  356. examples/task_apps/sokoban/filter_sft.toml +5 -0
  357. examples/task_apps/sokoban/task_app.py +1058 -0
  358. examples/task_apps/sokoban/tests/__init__.py +4 -0
  359. examples/task_apps/sokoban/tests/conftest.py +113 -0
  360. examples/task_apps/sokoban/tests/integration/__init__.py +4 -0
  361. examples/task_apps/sokoban/tests/integration/test_sokoban_eval.py +57 -0
  362. examples/task_apps/sokoban/tests/integration/test_sokoban_rollout.py +198 -0
  363. examples/task_apps/sokoban/tests/unit/__init__.py +4 -0
  364. examples/task_apps/sokoban/tests/unit/test_sokoban_environment.py +114 -0
  365. examples/task_apps/verilog/__init__.py +1 -0
  366. examples/task_apps/verilog/eval_groq_qwen32b.toml +22 -0
  367. examples/task_apps/verilog/filter_sft.toml +5 -0
  368. examples/task_apps/verilog/task_app/README.md +12 -0
  369. examples/task_apps/verilog/task_app/__init__.py +1 -0
  370. examples/task_apps/verilog/task_app/grpo_verilog.py +1166 -0
  371. examples/task_apps/verilog/task_app/grpo_verilog_task_app.py +145 -0
  372. examples/task_apps/verilog/tests/__init__.py +4 -0
  373. examples/task_apps/verilog/tests/conftest.py +115 -0
  374. examples/task_apps/verilog/tests/integration/__init__.py +4 -0
  375. examples/task_apps/verilog/tests/integration/test_verilog_eval.py +181 -0
  376. examples/task_apps/verilog/tests/integration/test_verilog_rollout.py +55 -0
  377. examples/task_apps/verilog/tests/unit/__init__.py +4 -0
  378. examples/task_apps/verilog/tests/unit/test_verilog_scoring.py +118 -0
  379. examples/tunnel_gepa_banking77/README.md +106 -0
  380. examples/tunnel_gepa_banking77/banking77_gepa_tunnel.toml +95 -0
  381. examples/tunnel_gepa_banking77/keep_tunnel_running.py +60 -0
  382. examples/tunnel_gepa_banking77/run_gepa_with_tunnel.sh +226 -0
  383. examples/vlm/PROPOSAL.md +53 -0
  384. examples/vlm/README.md +68 -0
  385. examples/vlm/configs/crafter_vlm_gpt4o.toml +49 -0
  386. examples/vlm/crafter_image_only_agent.py +207 -0
  387. examples/vlm/crafter_openai_vlm_agent.py +275 -0
  388. examples/vlm/filter_image_rows.py +63 -0
  389. examples/vlm/run_crafter_vlm_benchmark.py +316 -0
  390. examples/warming_up_to_rl/_utils.py +92 -0
  391. examples/warming_up_to_rl/analyze_trace_db.py +422 -0
  392. examples/warming_up_to_rl/configs/crafter_fft.toml +53 -0
  393. examples/warming_up_to_rl/configs/crafter_fft_4b.toml +54 -0
  394. examples/warming_up_to_rl/configs/eval_fft_qwen4b.toml +22 -0
  395. examples/warming_up_to_rl/configs/eval_groq_qwen32b.toml +15 -0
  396. examples/warming_up_to_rl/configs/eval_modal_qwen4b.toml +24 -0
  397. examples/warming_up_to_rl/configs/eval_stepwise_complex.toml +35 -0
  398. examples/warming_up_to_rl/configs/eval_stepwise_consistent.toml +26 -0
  399. examples/warming_up_to_rl/configs/eval_stepwise_per_achievement.toml +36 -0
  400. examples/warming_up_to_rl/configs/eval_stepwise_simple.toml +32 -0
  401. examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +85 -0
  402. examples/warming_up_to_rl/configs/rl_from_ft.toml +58 -0
  403. examples/warming_up_to_rl/export_trace_sft.py +837 -0
  404. examples/warming_up_to_rl/groq_test.py +97 -0
  405. examples/warming_up_to_rl/manage_secrets.py +131 -0
  406. examples/warming_up_to_rl/old/event_rewards.md +234 -0
  407. examples/warming_up_to_rl/old/notes.md +73 -0
  408. examples/warming_up_to_rl/readme.md +110 -0
  409. examples/warming_up_to_rl/run_eval.py +736 -0
  410. examples/warming_up_to_rl/run_fft_and_save.py +380 -0
  411. examples/warming_up_to_rl/run_local_rollout.py +239 -0
  412. examples/warming_up_to_rl/run_local_rollout_modal.py +248 -0
  413. examples/warming_up_to_rl/run_local_rollout_parallel.py +405 -0
  414. examples/warming_up_to_rl/run_local_rollout_traced.py +477 -0
  415. examples/warming_up_to_rl/run_rl_and_save.py +124 -0
  416. examples/warming_up_to_rl/run_rollout_remote.py +156 -0
  417. examples/warming_up_to_rl/task_app/README.md +42 -0
  418. examples/warming_up_to_rl/task_app/grpo_crafter.py +876 -0
  419. examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +135 -0
  420. examples/warming_up_to_rl/task_app/synth_envs_hosted/README.md +173 -0
  421. examples/warming_up_to_rl/task_app/synth_envs_hosted/__init__.py +5 -0
  422. examples/warming_up_to_rl/task_app/synth_envs_hosted/branching.py +143 -0
  423. examples/warming_up_to_rl/task_app/synth_envs_hosted/environment_routes.py +1226 -0
  424. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/__init__.py +1 -0
  425. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/__init__.py +6 -0
  426. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/app.py +1 -0
  427. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/environment.py +522 -0
  428. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +454 -0
  429. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +108 -0
  430. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/shared.py +305 -0
  431. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/tools.py +47 -0
  432. examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +253 -0
  433. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/__init__.py +5 -0
  434. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +729 -0
  435. examples/warming_up_to_rl/task_app/synth_envs_hosted/main.py +100 -0
  436. examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +1114 -0
  437. examples/warming_up_to_rl/task_app/synth_envs_hosted/registry.py +195 -0
  438. examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +1891 -0
  439. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/__init__.py +5 -0
  440. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/volume.py +211 -0
  441. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_agents.py +161 -0
  442. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_service.py +137 -0
  443. examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +129 -0
  444. examples/workflows/math_rl/configs/eval_base_qwen.toml +15 -0
  445. examples/workflows/math_rl/configs/eval_rl_qwen.toml +11 -0
  446. examples/workflows/math_rl/configs/rl_from_base_qwen.toml +62 -0
  447. examples/workflows/math_rl/configs/rl_from_base_qwen17.toml +80 -0
  448. examples/workflows/math_rl/configs/rl_from_ft_qwen.toml +35 -0
  449. examples/workflows/math_rl/download_dataset.py +80 -0
  450. examples/workflows/math_rl/run_eval.py +436 -0
  451. examples/workflows/math_rl/run_rl_and_save.py +111 -0
  452. synth_ai/__init__.py +47 -23
  453. synth_ai/_utils/__init__.py +47 -0
  454. synth_ai/_utils/base_url.py +10 -0
  455. synth_ai/_utils/http.py +10 -0
  456. synth_ai/_utils/prompts.py +10 -0
  457. synth_ai/_utils/task_app_state.py +12 -0
  458. synth_ai/_utils/user_config.py +10 -0
  459. synth_ai/api/models/supported.py +514 -0
  460. synth_ai/api/train/__init__.py +63 -0
  461. synth_ai/api/train/builders.py +473 -0
  462. synth_ai/api/train/cli.py +1185 -0
  463. synth_ai/api/train/config_finder.py +246 -0
  464. synth_ai/api/train/configs/__init__.py +65 -0
  465. synth_ai/api/train/configs/prompt_learning.py +496 -0
  466. synth_ai/api/train/configs/rl.py +188 -0
  467. synth_ai/api/train/configs/sft.py +99 -0
  468. synth_ai/api/train/configs/shared.py +81 -0
  469. synth_ai/api/train/env_resolver.py +352 -0
  470. synth_ai/api/train/pollers.py +91 -0
  471. synth_ai/api/train/prompt_learning.py +425 -0
  472. synth_ai/api/train/sft.py +390 -0
  473. synth_ai/api/train/supported_algos.py +147 -0
  474. synth_ai/api/train/task_app.py +195 -0
  475. synth_ai/api/train/utils.py +244 -0
  476. synth_ai/api/train/validators.py +1117 -0
  477. synth_ai/api/tunnel.py +49 -0
  478. synth_ai/auth/credentials.py +94 -0
  479. synth_ai/baseline/__init__.py +25 -0
  480. synth_ai/baseline/config.py +209 -0
  481. synth_ai/baseline/discovery.py +214 -0
  482. synth_ai/baseline/execution.py +146 -0
  483. synth_ai/cfgs.py +227 -0
  484. synth_ai/cli/__init__.py +90 -45
  485. synth_ai/cli/_modal_wrapper.py +31 -0
  486. synth_ai/cli/_storage.py +20 -0
  487. synth_ai/cli/_typer_patch.py +47 -0
  488. synth_ai/cli/_validate_task_app.py +29 -0
  489. synth_ai/cli/balance.py +16 -4
  490. synth_ai/cli/calc.py +36 -21
  491. synth_ai/cli/claude.py +70 -0
  492. synth_ai/cli/codex.py +267 -0
  493. synth_ai/cli/commands/__init__.py +18 -0
  494. synth_ai/cli/commands/baseline/__init__.py +12 -0
  495. synth_ai/cli/commands/baseline/core.py +637 -0
  496. synth_ai/cli/commands/baseline/list.py +93 -0
  497. synth_ai/cli/commands/demo/__init__.py +6 -0
  498. synth_ai/cli/commands/demo/core.py +163 -0
  499. synth_ai/cli/commands/eval/__init__.py +19 -0
  500. synth_ai/cli/commands/eval/core.py +1112 -0
  501. synth_ai/cli/commands/eval/errors.py +81 -0
  502. synth_ai/cli/commands/eval/validation.py +133 -0
  503. synth_ai/cli/commands/filter/__init__.py +12 -0
  504. synth_ai/cli/commands/filter/core.py +424 -0
  505. synth_ai/cli/commands/filter/errors.py +55 -0
  506. synth_ai/cli/commands/filter/validation.py +77 -0
  507. synth_ai/cli/commands/help/__init__.py +185 -0
  508. synth_ai/cli/commands/help/core.py +72 -0
  509. synth_ai/cli/commands/smoke/__init__.py +7 -0
  510. synth_ai/cli/commands/smoke/core.py +1437 -0
  511. synth_ai/cli/commands/status/__init__.py +66 -0
  512. synth_ai/cli/commands/status/client.py +192 -0
  513. synth_ai/cli/commands/status/config.py +92 -0
  514. synth_ai/cli/commands/status/errors.py +20 -0
  515. synth_ai/cli/commands/status/formatters.py +164 -0
  516. synth_ai/cli/commands/status/subcommands/__init__.py +9 -0
  517. synth_ai/cli/commands/status/subcommands/files.py +79 -0
  518. synth_ai/cli/commands/status/subcommands/jobs.py +334 -0
  519. synth_ai/cli/commands/status/subcommands/models.py +79 -0
  520. synth_ai/cli/commands/status/subcommands/pricing.py +22 -0
  521. synth_ai/cli/commands/status/subcommands/runs.py +81 -0
  522. synth_ai/cli/commands/status/subcommands/session.py +183 -0
  523. synth_ai/cli/commands/status/subcommands/summary.py +47 -0
  524. synth_ai/cli/commands/status/subcommands/usage.py +203 -0
  525. synth_ai/cli/commands/status/utils.py +114 -0
  526. synth_ai/cli/commands/train/__init__.py +53 -0
  527. synth_ai/cli/commands/train/core.py +21 -0
  528. synth_ai/cli/commands/train/errors.py +117 -0
  529. synth_ai/cli/commands/train/judge_schemas.py +200 -0
  530. synth_ai/cli/commands/train/judge_validation.py +305 -0
  531. synth_ai/cli/commands/train/validation.py +386 -0
  532. synth_ai/cli/demo.py +32 -140
  533. synth_ai/cli/deploy.py +233 -0
  534. synth_ai/cli/eval/__init__.py +36 -0
  535. synth_ai/cli/eval/core.py +5 -0
  536. synth_ai/cli/eval/errors.py +31 -0
  537. synth_ai/cli/eval/validation.py +5 -0
  538. synth_ai/cli/filter/__init__.py +28 -0
  539. synth_ai/cli/filter/core.py +5 -0
  540. synth_ai/cli/filter/errors.py +23 -0
  541. synth_ai/cli/filter/validation.py +5 -0
  542. synth_ai/cli/legacy_root_backup.py +28 -22
  543. synth_ai/cli/lib/__init__.py +10 -0
  544. synth_ai/cli/lib/task_app_discovery.py +7 -0
  545. synth_ai/cli/lib/task_app_env.py +518 -0
  546. synth_ai/cli/mcp.py +34 -0
  547. synth_ai/cli/modal_serve/__init__.py +12 -0
  548. synth_ai/cli/modal_serve/core.py +14 -0
  549. synth_ai/cli/modal_serve/errors.py +8 -0
  550. synth_ai/cli/modal_serve/validation.py +11 -0
  551. synth_ai/cli/opencode.py +256 -0
  552. synth_ai/cli/recent.py +13 -7
  553. synth_ai/cli/rl_demo.py +166 -114
  554. synth_ai/cli/root.py +143 -112
  555. synth_ai/cli/serve/__init__.py +12 -0
  556. synth_ai/cli/serve/core.py +14 -0
  557. synth_ai/cli/serve/errors.py +8 -0
  558. synth_ai/cli/serve/validation.py +11 -0
  559. synth_ai/cli/setup.py +49 -0
  560. synth_ai/cli/status.py +7 -125
  561. synth_ai/cli/task_app_deploy.py +7 -0
  562. synth_ai/cli/task_app_list.py +25 -0
  563. synth_ai/cli/task_app_modal_serve.py +11 -0
  564. synth_ai/cli/task_app_serve.py +11 -0
  565. synth_ai/cli/task_apps.py +3134 -0
  566. synth_ai/cli/traces.py +9 -5
  567. synth_ai/cli/train/__init__.py +12 -0
  568. synth_ai/cli/train/core.py +21 -0
  569. synth_ai/cli/train/errors.py +8 -0
  570. synth_ai/cli/train/validation.py +24 -0
  571. synth_ai/cli/train.py +5 -0
  572. synth_ai/cli/turso.py +73 -0
  573. synth_ai/cli/watch.py +13 -18
  574. synth_ai/demos/__init__.py +10 -0
  575. synth_ai/demos/core/__init__.py +28 -1
  576. synth_ai/demos/core/cli.py +745 -416
  577. synth_ai/demos/crafter/__init__.py +1 -0
  578. synth_ai/demos/crafter/crafter_fft_4b.toml +55 -0
  579. synth_ai/demos/crafter/grpo_crafter_task_app.py +185 -0
  580. synth_ai/demos/crafter/rl_from_base_qwen4b.toml +74 -0
  581. synth_ai/demos/demo_registry.py +176 -0
  582. synth_ai/demos/demo_task_apps/__init__.py +7 -1
  583. synth_ai/demos/demo_task_apps/core.py +75 -37
  584. synth_ai/demos/demo_task_apps/crafter/__init__.py +1 -0
  585. synth_ai/demos/demo_task_apps/crafter/configs/crafter_fft_4b.toml +53 -0
  586. synth_ai/demos/demo_task_apps/crafter/configs/rl_from_base_qwen4b.toml +73 -0
  587. synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +184 -0
  588. synth_ai/demos/demo_task_apps/math/_common.py +1 -2
  589. synth_ai/demos/demo_task_apps/math/app.py +2 -1
  590. synth_ai/demos/demo_task_apps/math/config.toml +55 -110
  591. synth_ai/demos/demo_task_apps/math/deploy_modal.py +3 -6
  592. synth_ai/demos/demo_task_apps/math/modal_task_app.py +491 -166
  593. synth_ai/demos/demo_task_apps/math/task_app_entry.py +37 -0
  594. synth_ai/demos/math/__init__.py +1 -0
  595. synth_ai/demos/math/_common.py +16 -0
  596. synth_ai/demos/math/app.py +38 -0
  597. synth_ai/demos/math/config.toml +76 -0
  598. synth_ai/demos/math/deploy_modal.py +54 -0
  599. synth_ai/demos/math/modal_task_app.py +703 -0
  600. synth_ai/demos/math/task_app_entry.py +51 -0
  601. synth_ai/environments/environment/core.py +7 -1
  602. synth_ai/environments/examples/bandit/engine.py +12 -5
  603. synth_ai/environments/examples/bandit/environment.py +0 -1
  604. synth_ai/environments/examples/bandit/taskset.py +4 -4
  605. synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +7 -4
  606. synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +9 -5
  607. synth_ai/environments/examples/crafter_classic/environment.py +93 -2
  608. synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +4 -3
  609. synth_ai/environments/examples/enron/engine.py +7 -2
  610. synth_ai/environments/examples/enron/environment.py +68 -0
  611. synth_ai/environments/examples/red/engine.py +60 -12
  612. synth_ai/environments/examples/red/engine_helpers/memory_map.py +7 -0
  613. synth_ai/environments/examples/red/engine_helpers/reward_components.py +151 -179
  614. synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_progression.py +477 -0
  615. synth_ai/environments/examples/red/engine_helpers/state_extraction.py +32 -0
  616. synth_ai/environments/examples/red/environment.py +86 -0
  617. synth_ai/environments/examples/red/trace_hooks_v3.py +168 -0
  618. synth_ai/environments/examples/sokoban/taskset.py +116 -0
  619. synth_ai/environments/examples/verilog/engine.py +104 -12
  620. synth_ai/environments/examples/wordle/environment.py +0 -1
  621. synth_ai/environments/reproducibility/tree.py +5 -6
  622. synth_ai/environments/service/app.py +11 -12
  623. synth_ai/environments/service/core_routes.py +10 -9
  624. synth_ai/environments/stateful/engine.py +1 -1
  625. synth_ai/environments/tasks/core.py +1 -0
  626. synth_ai/environments/tasks/filters.py +5 -6
  627. synth_ai/environments/tasks/utils.py +4 -5
  628. synth_ai/evals/__init__.py +15 -0
  629. synth_ai/evals/base.py +14 -5
  630. synth_ai/evals/client.py +82 -0
  631. synth_ai/evals/types.py +42 -0
  632. synth_ai/http.py +8 -22
  633. synth_ai/http_client.py +45 -12
  634. synth_ai/inference/__init__.py +0 -2
  635. synth_ai/inference/client.py +21 -7
  636. synth_ai/jobs/client.py +129 -80
  637. synth_ai/judge_schemas.py +127 -0
  638. synth_ai/learning/__init__.py +51 -6
  639. synth_ai/learning/algorithms.py +14 -0
  640. synth_ai/learning/client.py +122 -30
  641. synth_ai/learning/config.py +2 -40
  642. synth_ai/learning/constants.py +0 -2
  643. synth_ai/learning/ft_client.py +4 -56
  644. synth_ai/learning/health.py +14 -8
  645. synth_ai/learning/jobs.py +43 -47
  646. synth_ai/learning/prompt_learning_client.py +276 -0
  647. synth_ai/learning/prompt_learning_types.py +185 -0
  648. synth_ai/{rl → learning/rl}/__init__.py +14 -5
  649. synth_ai/learning/rl/client.py +269 -0
  650. synth_ai/learning/rl/config.py +31 -0
  651. synth_ai/{rl → learning/rl}/contracts.py +5 -10
  652. synth_ai/{rl → learning/rl}/env_keys.py +45 -16
  653. synth_ai/learning/rl/secrets.py +13 -0
  654. synth_ai/learning/rl_client.py +2 -253
  655. synth_ai/learning/sft/__init__.py +29 -0
  656. synth_ai/learning/sft/client.py +68 -0
  657. synth_ai/learning/sft/config.py +270 -0
  658. synth_ai/learning/sft/data.py +698 -0
  659. synth_ai/learning/sse.py +25 -26
  660. synth_ai/learning/validators.py +29 -25
  661. synth_ai/mcp/__init__.py +5 -0
  662. synth_ai/mcp/__main__.py +8 -0
  663. synth_ai/mcp/main.py +254 -0
  664. synth_ai/mcp/setup.py +100 -0
  665. synth_ai/modal.py +257 -0
  666. synth_ai/pricing/__init__.py +3 -0
  667. synth_ai/pricing/model_pricing.py +64 -0
  668. synth_ai/session/__init__.py +75 -0
  669. synth_ai/session/client.py +383 -0
  670. synth_ai/session/constants.py +63 -0
  671. synth_ai/session/exceptions.py +105 -0
  672. synth_ai/session/manager.py +139 -0
  673. synth_ai/session/models.py +89 -0
  674. synth_ai/session/query.py +110 -0
  675. synth_ai/spec/__init__.py +46 -0
  676. synth_ai/spec/dataclasses.py +149 -0
  677. synth_ai/spec/loader.py +144 -0
  678. synth_ai/spec/serializer.py +199 -0
  679. synth_ai/spec/validation.py +250 -0
  680. synth_ai/streaming/__init__.py +29 -0
  681. synth_ai/streaming/config.py +94 -0
  682. synth_ai/streaming/handlers.py +589 -0
  683. synth_ai/streaming/streamer.py +320 -0
  684. synth_ai/streaming/types.py +95 -0
  685. synth_ai/task/__init__.py +116 -3
  686. synth_ai/task/apps/__init__.py +132 -0
  687. synth_ai/task/auth.py +165 -0
  688. synth_ai/task/client.py +167 -0
  689. synth_ai/task/config.py +261 -0
  690. synth_ai/task/contracts.py +173 -57
  691. synth_ai/task/datasets.py +108 -0
  692. synth_ai/task/errors.py +50 -0
  693. synth_ai/task/health.py +17 -11
  694. synth_ai/task/inference_api.py +101 -0
  695. synth_ai/task/json.py +111 -0
  696. synth_ai/task/proxy.py +251 -0
  697. synth_ai/task/rubrics/__init__.py +55 -0
  698. synth_ai/task/rubrics/loaders.py +156 -0
  699. synth_ai/task/rubrics/models.py +57 -0
  700. synth_ai/task/rubrics/scoring.py +116 -0
  701. synth_ai/task/rubrics/strict.py +149 -0
  702. synth_ai/task/rubrics.py +219 -0
  703. synth_ai/task/server.py +432 -0
  704. synth_ai/task/trace_correlation_helpers.py +328 -0
  705. synth_ai/task/tracing_utils.py +95 -0
  706. synth_ai/task/validators.py +449 -6
  707. synth_ai/task/vendors.py +59 -0
  708. synth_ai/tracing_v3/__init__.py +4 -0
  709. synth_ai/tracing_v3/abstractions.py +21 -4
  710. synth_ai/tracing_v3/config.py +167 -22
  711. synth_ai/tracing_v3/constants.py +21 -0
  712. synth_ai/tracing_v3/db_config.py +42 -29
  713. synth_ai/tracing_v3/decorators.py +80 -45
  714. synth_ai/tracing_v3/examples/basic_usage.py +15 -9
  715. synth_ai/tracing_v3/hooks.py +6 -4
  716. synth_ai/tracing_v3/llm_call_record_helpers.py +161 -61
  717. synth_ai/tracing_v3/migration_helper.py +1 -2
  718. synth_ai/tracing_v3/replica_sync.py +12 -7
  719. synth_ai/tracing_v3/serialization.py +130 -0
  720. synth_ai/tracing_v3/session_tracer.py +86 -21
  721. synth_ai/tracing_v3/storage/base.py +98 -12
  722. synth_ai/tracing_v3/storage/config.py +63 -16
  723. synth_ai/tracing_v3/storage/factory.py +11 -9
  724. synth_ai/tracing_v3/storage/utils.py +15 -11
  725. synth_ai/tracing_v3/trace_utils.py +317 -0
  726. synth_ai/tracing_v3/turso/__init__.py +8 -21
  727. synth_ai/tracing_v3/turso/daemon.py +123 -15
  728. synth_ai/tracing_v3/turso/models.py +5 -2
  729. synth_ai/tracing_v3/turso/native_manager.py +1293 -0
  730. synth_ai/tracing_v3/utils.py +5 -4
  731. synth_ai/tunnel.py +143 -0
  732. synth_ai/tunnel_deploy.py +278 -0
  733. synth_ai/types.py +8 -0
  734. synth_ai/urls.py +11 -0
  735. synth_ai/utils/__init__.py +166 -0
  736. synth_ai/utils/agents.py +74 -0
  737. synth_ai/utils/apps.py +152 -0
  738. synth_ai/utils/base_url.py +94 -0
  739. synth_ai/utils/bin.py +39 -0
  740. synth_ai/utils/claude.py +36 -0
  741. synth_ai/utils/cli.py +284 -0
  742. synth_ai/utils/config.py +81 -0
  743. synth_ai/utils/env.py +346 -0
  744. synth_ai/utils/errors.py +85 -0
  745. synth_ai/utils/http.py +172 -0
  746. synth_ai/utils/json.py +72 -0
  747. synth_ai/utils/log_filter.py +99 -0
  748. synth_ai/utils/logging.py +198 -0
  749. synth_ai/utils/modal.py +299 -0
  750. synth_ai/utils/paths.py +95 -0
  751. synth_ai/utils/process.py +233 -0
  752. synth_ai/utils/prompts.py +39 -0
  753. synth_ai/utils/sqld.py +122 -0
  754. synth_ai/utils/ssl.py +25 -0
  755. synth_ai/utils/task_app_discovery.py +882 -0
  756. synth_ai/utils/task_app_env.py +186 -0
  757. synth_ai/utils/task_app_state.py +318 -0
  758. synth_ai/utils/tunnel/__init__.py +12 -0
  759. synth_ai/utils/tunnel/config.py +55 -0
  760. synth_ai/utils/user_config.py +137 -0
  761. synth_ai/uvicorn.py +77 -0
  762. synth_ai-0.2.23.dev3.dist-info/METADATA +357 -0
  763. synth_ai-0.2.23.dev3.dist-info/RECORD +983 -0
  764. {synth_ai-0.2.8.dev4.dist-info → synth_ai-0.2.23.dev3.dist-info}/entry_points.txt +0 -1
  765. {synth_ai-0.2.8.dev4.dist-info → synth_ai-0.2.23.dev3.dist-info}/top_level.txt +1 -0
  766. synth_ai/cli/man.py +0 -106
  767. synth_ai/core/experiment.py +0 -15
  768. synth_ai/core/system.py +0 -15
  769. synth_ai/environments/examples/sokoban/units/astar_common.py +0 -95
  770. synth_ai/experimental/synth_oss.py +0 -446
  771. synth_ai/handshake.py +0 -63
  772. synth_ai/install_sqld.sh +0 -40
  773. synth_ai/learning/offline/dpo.py +0 -0
  774. synth_ai/learning/offline/providers.py +0 -7
  775. synth_ai/learning/offline/sft.py +0 -0
  776. synth_ai/learning/offline/shared.py +0 -0
  777. synth_ai/learning/online/grpo.py +0 -0
  778. synth_ai/learning/online/irft.py +0 -0
  779. synth_ai/learning/prompts/banking77_injection_eval.py +0 -168
  780. synth_ai/learning/prompts/gepa.py +0 -0
  781. synth_ai/learning/prompts/hello_world_in_context_injection_ex.py +0 -213
  782. synth_ai/learning/prompts/mipro.py +0 -289
  783. synth_ai/learning/prompts/random_search.py +0 -246
  784. synth_ai/learning/prompts/run_mipro_banking77.py +0 -172
  785. synth_ai/learning/prompts/run_random_search_banking77.py +0 -324
  786. synth_ai/lm/__init__.py +0 -51
  787. synth_ai/lm/caching/constants.py +0 -6
  788. synth_ai/lm/caching/dbs.py +0 -0
  789. synth_ai/lm/caching/ephemeral.py +0 -102
  790. synth_ai/lm/caching/handler.py +0 -137
  791. synth_ai/lm/caching/initialize.py +0 -11
  792. synth_ai/lm/caching/persistent.py +0 -114
  793. synth_ai/lm/config.py +0 -110
  794. synth_ai/lm/constants.py +0 -32
  795. synth_ai/lm/core/__init__.py +0 -8
  796. synth_ai/lm/core/all.py +0 -73
  797. synth_ai/lm/core/exceptions.py +0 -7
  798. synth_ai/lm/core/main.py +0 -319
  799. synth_ai/lm/core/main_v3.py +0 -594
  800. synth_ai/lm/core/synth_models.py +0 -48
  801. synth_ai/lm/core/vendor_clients.py +0 -188
  802. synth_ai/lm/cost/monitor.py +0 -1
  803. synth_ai/lm/cost/statefulness.py +0 -1
  804. synth_ai/lm/injection.py +0 -80
  805. synth_ai/lm/overrides.py +0 -206
  806. synth_ai/lm/provider_support/__init__.py +0 -8
  807. synth_ai/lm/provider_support/anthropic.py +0 -972
  808. synth_ai/lm/provider_support/openai.py +0 -1139
  809. synth_ai/lm/provider_support/suppress_logging.py +0 -31
  810. synth_ai/lm/structured_outputs/handler.py +0 -440
  811. synth_ai/lm/structured_outputs/inject.py +0 -297
  812. synth_ai/lm/structured_outputs/rehabilitate.py +0 -185
  813. synth_ai/lm/tools/__init__.py +0 -3
  814. synth_ai/lm/tools/base.py +0 -172
  815. synth_ai/lm/unified_interface.py +0 -202
  816. synth_ai/lm/vendors/base.py +0 -81
  817. synth_ai/lm/vendors/core/anthropic_api.py +0 -387
  818. synth_ai/lm/vendors/core/gemini_api.py +0 -292
  819. synth_ai/lm/vendors/core/mistral_api.py +0 -322
  820. synth_ai/lm/vendors/core/openai_api.py +0 -225
  821. synth_ai/lm/vendors/core/synth_dev_api.py +0 -0
  822. synth_ai/lm/vendors/local/ollama.py +0 -0
  823. synth_ai/lm/vendors/openai_standard.py +0 -780
  824. synth_ai/lm/vendors/openai_standard_responses.py +0 -256
  825. synth_ai/lm/vendors/retries.py +0 -22
  826. synth_ai/lm/vendors/supported/custom_endpoint.py +0 -417
  827. synth_ai/lm/vendors/supported/deepseek.py +0 -69
  828. synth_ai/lm/vendors/supported/grok.py +0 -75
  829. synth_ai/lm/vendors/supported/groq.py +0 -16
  830. synth_ai/lm/vendors/supported/ollama.py +0 -15
  831. synth_ai/lm/vendors/supported/openrouter.py +0 -74
  832. synth_ai/lm/vendors/supported/together.py +0 -11
  833. synth_ai/lm/vendors/synth_client.py +0 -808
  834. synth_ai/lm/warmup.py +0 -186
  835. synth_ai/rl/secrets.py +0 -19
  836. synth_ai/scripts/verify_rewards.py +0 -100
  837. synth_ai/tracing/__init__.py +0 -30
  838. synth_ai/tracing_v1/__init__.py +0 -33
  839. synth_ai/tracing_v3/turso/manager.py +0 -760
  840. synth_ai/v0/tracing/abstractions.py +0 -224
  841. synth_ai/v0/tracing/base_client.py +0 -91
  842. synth_ai/v0/tracing/client_manager.py +0 -131
  843. synth_ai/v0/tracing/config.py +0 -142
  844. synth_ai/v0/tracing/context.py +0 -146
  845. synth_ai/v0/tracing/decorators.py +0 -682
  846. synth_ai/v0/tracing/events/__init__.py +0 -0
  847. synth_ai/v0/tracing/events/manage.py +0 -147
  848. synth_ai/v0/tracing/events/scope.py +0 -86
  849. synth_ai/v0/tracing/events/store.py +0 -228
  850. synth_ai/v0/tracing/immediate_client.py +0 -151
  851. synth_ai/v0/tracing/local.py +0 -18
  852. synth_ai/v0/tracing/log_client_base.py +0 -73
  853. synth_ai/v0/tracing/retry_queue.py +0 -186
  854. synth_ai/v0/tracing/trackers.py +0 -515
  855. synth_ai/v0/tracing/upload.py +0 -512
  856. synth_ai/v0/tracing/utils.py +0 -9
  857. synth_ai/v0/tracing_v1/__init__.py +0 -16
  858. synth_ai/v0/tracing_v1/abstractions.py +0 -224
  859. synth_ai/v0/tracing_v1/base_client.py +0 -91
  860. synth_ai/v0/tracing_v1/client_manager.py +0 -131
  861. synth_ai/v0/tracing_v1/config.py +0 -142
  862. synth_ai/v0/tracing_v1/context.py +0 -146
  863. synth_ai/v0/tracing_v1/decorators.py +0 -703
  864. synth_ai/v0/tracing_v1/events/__init__.py +0 -0
  865. synth_ai/v0/tracing_v1/events/manage.py +0 -147
  866. synth_ai/v0/tracing_v1/events/scope.py +0 -86
  867. synth_ai/v0/tracing_v1/events/store.py +0 -228
  868. synth_ai/v0/tracing_v1/immediate_client.py +0 -151
  869. synth_ai/v0/tracing_v1/local.py +0 -18
  870. synth_ai/v0/tracing_v1/log_client_base.py +0 -73
  871. synth_ai/v0/tracing_v1/retry_queue.py +0 -186
  872. synth_ai/v0/tracing_v1/trackers.py +0 -515
  873. synth_ai/v0/tracing_v1/upload.py +0 -527
  874. synth_ai/v0/tracing_v1/utils.py +0 -9
  875. synth_ai/zyk/__init__.py +0 -30
  876. synth_ai-0.2.8.dev4.dist-info/METADATA +0 -129
  877. synth_ai-0.2.8.dev4.dist-info/RECORD +0 -420
  878. {synth_ai/lm/caching → examples/task_apps}/__init__.py +0 -0
  879. {synth_ai/lm/cost → examples/task_apps/crafter}/__init__.py +0 -0
  880. {synth_ai/lm/structured_outputs → examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server}/__init__.py +0 -0
  881. {synth_ai/lm/vendors → examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests}/__init__.py +0 -0
  882. {synth_ai/lm/vendors/core → examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils}/__init__.py +0 -0
  883. {synth_ai/lm/vendors/local → examples/task_apps/math}/__init__.py +0 -0
  884. {synth_ai/lm/vendors/supported → examples/workflows}/__init__.py +0 -0
  885. {synth_ai/v0/tracing → examples/workflows/math_rl}/__init__.py +0 -0
  886. /synth_ai/{compound/cais.py → cli/__main__.py} +0 -0
  887. /synth_ai/{learning/filtering.py → py.typed} +0 -0
  888. {synth_ai-0.2.8.dev4.dist-info → synth_ai-0.2.23.dev3.dist-info}/WHEEL +0 -0
  889. {synth_ai-0.2.8.dev4.dist-info → synth_ai-0.2.23.dev3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1437 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import logging
6
+ import os
7
+ import subprocess
8
+ import sys
9
+ import time
10
+ import uuid
11
+ from pathlib import Path
12
+ from typing import Any
13
+ from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
14
+
15
+ import click
16
+ import httpx
17
+ import tomllib
18
+ from synth_ai.task.client import TaskAppClient
19
+ from synth_ai.task.contracts import (
20
+ RolloutEnvSpec,
21
+ RolloutMode,
22
+ RolloutPolicySpec,
23
+ RolloutRecordConfig,
24
+ RolloutRequest,
25
+ RolloutSafetyConfig,
26
+ )
27
+ from synth_ai.task.validators import (
28
+ normalize_inference_url,
29
+ validate_rollout_response_for_rl,
30
+ validate_task_app_url,
31
+ )
32
+ from synth_ai.tracing_v3.config import resolve_trace_db_settings
33
+ from synth_ai.tracing_v3.turso.daemon import start_sqld
34
+
35
+
36
+ def _append_query_param(url: str, key: str, value: str) -> str:
37
+ parsed = urlparse(url)
38
+ params = dict(parse_qsl(parsed.query, keep_blank_values=True))
39
+ params[key] = value
40
+ new_query = urlencode(params)
41
+ result = urlunparse(parsed._replace(query=new_query))
42
+ return str(result)
43
+
44
+
45
+ def _ensure_local_libsql() -> None:
46
+ """Start a local sqld/libSQL instance or abort the smoke test."""
47
+
48
+ traces_root = Path(os.getenv("SYNTH_TRACES_DIR", str((Path.cwd() / "traces" / "v3").resolve())))
49
+ traces_root.mkdir(parents=True, exist_ok=True)
50
+
51
+ local_db_path = Path(os.getenv("SQLD_DB_PATH", str(traces_root / "local.db"))).resolve()
52
+ local_db_path.parent.mkdir(parents=True, exist_ok=True)
53
+
54
+ hrana_port = int(os.getenv("SQLD_HTTP_PORT", "8080"))
55
+ http_port = hrana_port + 1
56
+ os.environ["SQLD_DB_PATH"] = str(local_db_path)
57
+ os.environ["SQLD_HTTP_PORT"] = str(hrana_port)
58
+
59
+ try:
60
+ start_sqld(db_path=str(local_db_path), hrana_port=hrana_port, http_port=http_port)
61
+ started_new = True
62
+ except Exception as exc:
63
+ # If address in use, assume an existing sqld instance; verify health below
64
+ if "Address already in use" in str(exc):
65
+ started_new = False
66
+ click.echo(
67
+ f"[libsql] sqld already running on 127.0.0.1:{hrana_port} (hrana) and 127.0.0.1:{http_port} (http); attempting to reuse", err=True
68
+ )
69
+ else:
70
+ raise click.ClickException(
71
+ f"Failed to start local sqld on 127.0.0.1:{hrana_port}: {exc}"
72
+ ) from exc
73
+
74
+ health_url = f"http://127.0.0.1:{http_port}/health"
75
+ deadline = time.time() + 5.0
76
+ healthy = False
77
+ while time.time() < deadline:
78
+ try:
79
+ resp = httpx.get(health_url, timeout=0.5)
80
+ if resp.status_code == 200:
81
+ healthy = True
82
+ break
83
+ except Exception:
84
+ pass
85
+ time.sleep(0.1)
86
+
87
+ if not healthy:
88
+ msg = (
89
+ f"Tracing backend not reachable at {health_url}. "
90
+ "Start sqld manually or disable tracing (TASKAPP_TRACING_ENABLED=0)."
91
+ )
92
+ raise click.ClickException(msg)
93
+
94
+ click.echo(
95
+ f"[libsql] sqld ready on libsql://127.0.0.1:{hrana_port} with HTTP API on :{http_port} (started_new={started_new})",
96
+ err=True,
97
+ )
98
+
99
+ # Python libsql client uses HTTP API port, not Hrana WebSocket port
100
+ local_dsn = f"http://127.0.0.1:{http_port}"
101
+ os.environ["LIBSQL_URL"] = local_dsn
102
+ os.environ["SYNTH_TRACES_DB"] = local_dsn
103
+ os.environ.pop("LIBSQL_AUTH_TOKEN", None)
104
+ os.environ.pop("TURSO_AUTH_TOKEN", None)
105
+
106
+
107
+ def _refresh_tracing_config() -> None:
108
+ """Rebuild global tracing configuration so new env vars take effect."""
109
+
110
+ from synth_ai.tracing_v3 import config as tracing_config_module
111
+ from synth_ai.tracing_v3.storage import config as storage_config_module
112
+
113
+ tracing_config_module.CONFIG = tracing_config_module.TursoConfig() # type: ignore[assignment]
114
+ storage_config_module.STORAGE_CONFIG = storage_config_module.StorageConfig( # type: ignore[assignment]
115
+ connection_string=os.environ["SYNTH_TRACES_DB"],
116
+ backend=storage_config_module.StorageBackend.TURSO_NATIVE,
117
+ )
118
+
119
+
120
+ def _load_smoke_config(config_path: Path | None) -> dict[str, Any]:
121
+ """Load [smoke] section from TOML config file.
122
+
123
+ Returns an empty dict if no config file or no [smoke] section.
124
+ """
125
+ if not config_path:
126
+ return {}
127
+
128
+ try:
129
+ with open(config_path, "rb") as f:
130
+ full_config = tomllib.load(f)
131
+
132
+ smoke_config = full_config.get("smoke", {})
133
+
134
+ if smoke_config:
135
+ click.echo(f"[smoke] Loaded configuration from {config_path}", err=True)
136
+ click.echo(f"[smoke] Config keys: {', '.join(smoke_config.keys())}", err=True)
137
+
138
+ return smoke_config
139
+ except Exception as exc:
140
+ click.echo(f"[smoke] Warning: Failed to load config from {config_path}: {exc}", err=True)
141
+ return {}
142
+
143
+
144
+ def _kill_process_on_port(port: int) -> None:
145
+ """Kill any process listening on the given port."""
146
+ try:
147
+ # Use lsof to find and kill process on port
148
+ result = subprocess.run(
149
+ ["lsof", "-ti", f":{port}"],
150
+ capture_output=True,
151
+ text=True,
152
+ timeout=2,
153
+ )
154
+ if result.stdout.strip():
155
+ pids = result.stdout.strip().split('\n')
156
+ for pid in pids:
157
+ try:
158
+ subprocess.run(["kill", "-9", pid], timeout=2)
159
+ click.echo(f"[smoke] Killed existing process {pid} on port {port}", err=True)
160
+ except Exception:
161
+ pass
162
+ time.sleep(2.0) # Give OS time to release port
163
+ except Exception as exc:
164
+ click.echo(f"[smoke] Warning: Could not check/kill port {port}: {exc}", err=True)
165
+
166
+
167
+ def _start_task_app_server(
168
+ task_app_name: str,
169
+ port: int,
170
+ env_file: str | None,
171
+ force: bool
172
+ ) -> tuple[Any, str]:
173
+ """Start a task app server in the background using task-app serve.
174
+
175
+ Returns (process, url) tuple.
176
+ """
177
+ import subprocess
178
+ import time as time_module
179
+
180
+ # Build command using task-app serve (for TaskAppConfig-based apps)
181
+ cmd = [
182
+ "nohup",
183
+ "uvx", "synth-ai",
184
+ "task-app", "serve", task_app_name,
185
+ "--port", str(port),
186
+ ]
187
+
188
+ if env_file:
189
+ cmd.extend(["--env-file", env_file])
190
+
191
+ if force:
192
+ cmd.append("--force")
193
+
194
+ # Resolve the synth-ai root directory
195
+ import synth_ai
196
+ synth_ai_root = Path(synth_ai.__file__).resolve().parent.parent
197
+
198
+ click.echo(f"[smoke] Starting task app '{task_app_name}' on port {port}...", err=True)
199
+ click.echo(f"[smoke] Command: {' '.join(cmd)}", err=True)
200
+ click.echo(f"[smoke] Working directory: {synth_ai_root}", err=True)
201
+
202
+ # nohup requires output redirection to a file
203
+ # Open file, start process, then close file handle so process is fully detached
204
+ # Run from synth-ai root so task app discovery works
205
+ nohup_log = Path(synth_ai_root) / "nohup_task_app.out"
206
+
207
+ # Inherit SYNTH_QUIET environment variable to suppress patch messages
208
+ env = os.environ.copy()
209
+ if os.getenv("SYNTH_QUIET"):
210
+ env["SYNTH_QUIET"] = "1"
211
+
212
+ with open(nohup_log, "w") as log_file:
213
+ proc = subprocess.Popen(
214
+ cmd,
215
+ stdout=log_file,
216
+ stderr=subprocess.STDOUT,
217
+ text=True,
218
+ cwd=str(synth_ai_root),
219
+ env=env,
220
+ )
221
+ # File is closed immediately so process is detached
222
+
223
+ # Wait for server to be ready
224
+ url = f"http://localhost:{port}"
225
+ click.echo(f"[smoke] Waiting for task app to be ready at {url}...", err=True)
226
+
227
+ import httpx
228
+ deadline = time.time() + 120.0 # Give it 2 minutes for initial setup
229
+ attempt = 0
230
+ last_log_line = None
231
+ while time.time() < deadline:
232
+ attempt += 1
233
+ try:
234
+ resp = httpx.get(f"{url}/health", timeout=1.0)
235
+ # Accept both 200 and 400 - 400 means server is up but auth is failing (which is fine for smoke test)
236
+ if resp.status_code in (200, 400):
237
+ click.echo(f"[smoke] Task app ready at {url} (status={resp.status_code})", err=True)
238
+ return proc, url
239
+ except Exception:
240
+ pass
241
+
242
+ # Show polling progress every 5 seconds with last log line
243
+ if attempt % 10 == 0:
244
+ elapsed = int(time.time() - (deadline - 120.0))
245
+ # Try to read last line from nohup log
246
+ try:
247
+ if nohup_log.exists():
248
+ with open(nohup_log) as f:
249
+ lines = f.readlines()
250
+ if lines:
251
+ # Get last non-empty line
252
+ for line in reversed(lines[-10:]):
253
+ stripped = line.strip()
254
+ if stripped and stripped != last_log_line:
255
+ last_log_line = stripped
256
+ # Truncate if too long
257
+ if len(stripped) > 80:
258
+ stripped = stripped[:77] + "..."
259
+ click.echo(f"[smoke] Waiting ({elapsed}s): {stripped}", err=True)
260
+ break
261
+ else:
262
+ click.echo(f"[smoke] Still waiting for task app... ({elapsed}s elapsed)", err=True)
263
+ else:
264
+ click.echo(f"[smoke] Still waiting for task app... ({elapsed}s elapsed)", err=True)
265
+ except Exception:
266
+ click.echo(f"[smoke] Still waiting for task app... ({elapsed}s elapsed)", err=True)
267
+
268
+ # Check if process died
269
+ if proc.poll() is not None:
270
+ # Build a manual command that the user can copy-paste
271
+ manual_cmd_parts = ["uvx", "synth-ai", "task-app", "serve", task_app_name, "--port", str(port)]
272
+ if env_file:
273
+ manual_cmd_parts.extend(["--env-file", env_file])
274
+ if force:
275
+ manual_cmd_parts.append("--force")
276
+
277
+ raise click.ClickException(
278
+ f"Task app '{task_app_name}' process exited unexpectedly (code={proc.returncode}). "
279
+ f"Check that the task app name is correct and .env has required keys. "
280
+ f"Try running manually: {' '.join(manual_cmd_parts)}"
281
+ )
282
+
283
+ time_module.sleep(0.5)
284
+
285
+ proc.kill()
286
+ raise click.ClickException("Task app failed to start within 120 seconds")
287
+
288
+
289
+ def _start_sqld_server(
290
+ db_path: str,
291
+ hrana_port: int,
292
+ http_port: int
293
+ ) -> Any:
294
+ """Start sqld server in the background.
295
+
296
+ Returns the process handle.
297
+ """
298
+ import shutil
299
+ import subprocess
300
+
301
+ # Check if sqld is available
302
+ sqld_bin = shutil.which("sqld")
303
+ if not sqld_bin:
304
+ click.echo("[smoke] Warning: sqld not found in PATH, skipping auto-start", err=True)
305
+ click.echo("[smoke] Install sqld: brew install sqld", err=True)
306
+ return None
307
+
308
+ # Ensure db directory exists
309
+ db_path_obj = Path(db_path).expanduser().resolve()
310
+ db_path_obj.parent.mkdir(parents=True, exist_ok=True)
311
+
312
+ # Kill any existing processes on these ports
313
+ for port in [hrana_port, http_port]:
314
+ _kill_process_on_port(port)
315
+
316
+ cmd = [
317
+ sqld_bin,
318
+ "--db-path", str(db_path_obj),
319
+ "--hrana-listen-addr", f"127.0.0.1:{hrana_port}",
320
+ "--http-listen-addr", f"127.0.0.1:{http_port}",
321
+ ]
322
+
323
+ click.echo("[smoke] Starting sqld server...", err=True)
324
+ click.echo(f"[smoke] DB path: {db_path_obj}", err=True)
325
+ click.echo(f"[smoke] Hrana port: {hrana_port}, HTTP port: {http_port}", err=True)
326
+ click.echo(f"[smoke] Command: {' '.join(cmd)}", err=True)
327
+
328
+ # Redirect to devnull to avoid process dying from pipe buffer issues
329
+ proc = subprocess.Popen(
330
+ cmd,
331
+ stdout=subprocess.DEVNULL,
332
+ stderr=subprocess.DEVNULL,
333
+ text=True,
334
+ )
335
+
336
+ # Wait for server to be ready
337
+ health_url = f"http://127.0.0.1:{http_port}/health"
338
+ click.echo(f"[smoke] Waiting for sqld to be ready at {health_url}...", err=True)
339
+
340
+ deadline = time.time() + 10.0
341
+ while time.time() < deadline:
342
+ try:
343
+ resp = httpx.get(health_url, timeout=0.5)
344
+ if resp.status_code == 200:
345
+ click.echo("[smoke] sqld ready", err=True)
346
+ # Set environment variables for tracing
347
+ os.environ["SQLD_DB_PATH"] = str(db_path_obj)
348
+ os.environ["SQLD_HTTP_PORT"] = str(hrana_port)
349
+ os.environ["LIBSQL_URL"] = f"http://127.0.0.1:{http_port}"
350
+ os.environ["SYNTH_TRACES_DB"] = f"http://127.0.0.1:{http_port}"
351
+ return proc
352
+ except Exception:
353
+ pass
354
+
355
+ # Check if process died
356
+ if proc.poll() is not None:
357
+ click.echo(f"[smoke] Warning: sqld process exited with code {proc.returncode}", err=True)
358
+ return None
359
+
360
+ time.sleep(0.2)
361
+
362
+ click.echo("[smoke] Warning: sqld health check timed out, continuing anyway...", err=True)
363
+ return proc
364
+
365
+ class MockRLTrainer:
366
+ """Minimal trainer emulator with a local FastAPI mock for GPT-5-Nano.
367
+
368
+ In ``synthetic`` mode it emits deterministic tool calls so the rollout can
369
+ progress without relying on external inference. In ``openai`` mode it acts
370
+ as a thin proxy around the real OpenAI chat completions endpoint (useful to
371
+ reproduce production behaviour locally).
372
+ """
373
+
374
+ def __init__(self, *, port: int = 0, backend: str = "synthetic") -> None:
375
+ self.port = port
376
+ self.backend = backend.lower().strip() or "synthetic"
377
+ self._server = None
378
+ self._task: asyncio.Task | None = None
379
+ self._openai_endpoint = os.getenv(
380
+ "SMOKE_OPENAI_ENDPOINT", "https://api.openai.com/v1/chat/completions"
381
+ )
382
+ self._openai_api_key = (
383
+ os.getenv("SMOKE_OPENAI_API_KEY") or os.getenv("OPENAI_API_KEY") or ""
384
+ )
385
+
386
+ def _build_app(self):
387
+ import json
388
+
389
+ from fastapi import Body, FastAPI
390
+ from fastapi.responses import JSONResponse
391
+
392
+ try:
393
+ logger = logging.getLogger(__name__)
394
+ except Exception: # pragma: no cover - logging failures should not crash
395
+ logger = None
396
+
397
+ app = FastAPI()
398
+ backend = self.backend
399
+
400
+ @app.post("/v1/chat/completions")
401
+ async def chat_completions(body: dict = Body(...), cid: str | None = None):
402
+ log = logger or logging.getLogger("MockRLTrainer")
403
+ try:
404
+ msg_count = len(body.get("messages") or [])
405
+ except Exception:
406
+ msg_count = -1
407
+ click.echo(
408
+ f"[mock-rl] ← request backend={backend} model={body.get('model')} messages={msg_count} cid={cid}",
409
+ err=True,
410
+ )
411
+
412
+ # Explicit Body(...) avoids FastAPI interpreting parameters as query args
413
+ model = (body.get("model") or "gpt-5-nano")
414
+ messages = body.get("messages") or []
415
+ tools = body.get("tools") or []
416
+
417
+ # Decide whether to emit a tool call (to drive env steps) or plain text
418
+ emit_tool = False
419
+ tool_name = ""
420
+ for t in tools:
421
+ try:
422
+ if (t or {}).get("type") == "function":
423
+ fn = (t or {}).get("function") or {}
424
+ name = (fn or {}).get("name") or ""
425
+ if name:
426
+ tool_name = name
427
+ emit_tool = True
428
+ break
429
+ except Exception:
430
+ continue
431
+
432
+ # Simple heuristic actions to move/explore then interact
433
+ actions = ["move_right", "move_right", "move_down", "move_left", "do"]
434
+
435
+ correlation = cid
436
+
437
+ if backend == "openai":
438
+ if not self._openai_api_key:
439
+ return JSONResponse(
440
+ {
441
+ "error": "OPENAI_API_KEY (or SMOKE_OPENAI_API_KEY) is required for mock backend 'openai'"
442
+ },
443
+ status_code=500,
444
+ )
445
+ try:
446
+ from examples.task_apps.crafter.task_app.synth_envs_hosted.inference.openai_client import (
447
+ OpenAIClient as _HostedOpenAIClient,
448
+ )
449
+
450
+ hosted_client = _HostedOpenAIClient(
451
+ base_url=self._openai_endpoint,
452
+ api_key=self._openai_api_key,
453
+ )
454
+ except Exception as exc:
455
+ if logger is not None:
456
+ logger.error("MockRLTrainer failed to import HostedOpenAIClient: %s", exc)
457
+ return JSONResponse(
458
+ {"error": f"OpenAI proxy unavailable: {exc}"},
459
+ status_code=500,
460
+ )
461
+
462
+ try:
463
+ result = await hosted_client.generate_with_retries( # type: ignore[attr-defined]
464
+ request=body,
465
+ base_url=self._openai_endpoint,
466
+ max_retries=0,
467
+ )
468
+ except Exception as exc:
469
+ if logger is not None:
470
+ logger.error("MockRLTrainer OpenAI generate failed: %s", exc)
471
+ return JSONResponse(
472
+ {"error": f"OpenAI proxy request failed: {exc}"},
473
+ status_code=502,
474
+ )
475
+
476
+ if isinstance(result, dict):
477
+ data_typed = dict(result)
478
+ synth_meta = data_typed.get("synth")
479
+ if not isinstance(synth_meta, dict):
480
+ synth_meta = {}
481
+ data_typed["synth"] = synth_meta
482
+ if correlation:
483
+ synth_meta.setdefault("cid", correlation)
484
+
485
+ # Fallback: if the upstream response failed to emit tool calls,
486
+ # synthesize a deterministic action plan so the rollout can proceed.
487
+ try:
488
+ choices = data_typed.get("choices") or []
489
+ first = choices[0] if choices else {}
490
+ message = first.get("message") if isinstance(first, dict) else {}
491
+ tc = message.get("tool_calls") if isinstance(message, dict) else None
492
+ if not tc:
493
+ if logger is not None:
494
+ logger.warning(
495
+ "MockRLTrainer fallback: OpenAI returned no tool calls; injecting deterministic actions."
496
+ )
497
+ fallback_message = dict(message or {})
498
+ fallback_message.setdefault("role", "assistant")
499
+ fallback_message["content"] = ""
500
+ fallback_message["tool_calls"] = [
501
+ {
502
+ "id": f"call_{uuid.uuid4().hex[:8]}",
503
+ "type": "function",
504
+ "function": {
505
+ "name": tool_name or "interact_many",
506
+ "arguments": json.dumps({"actions": actions}),
507
+ },
508
+ }
509
+ ]
510
+ fallback_message["function_call"] = {
511
+ "name": tool_name or "interact_many",
512
+ "arguments": json.dumps({"actions": actions}),
513
+ }
514
+ if choices:
515
+ choices[0]["message"] = fallback_message
516
+ else:
517
+ data_typed["choices"] = [
518
+ {
519
+ "index": 0,
520
+ "message": fallback_message,
521
+ "finish_reason": "tool_calls",
522
+ }
523
+ ]
524
+ except Exception as exc:
525
+ if logger is not None:
526
+ logger.debug("MockRLTrainer fallback injection failed: %s", exc)
527
+
528
+ tool_call_count = 0
529
+ try:
530
+ choices = data_typed.get("choices") or []
531
+ first = choices[0] if choices else {}
532
+ message = first.get("message") if isinstance(first, dict) else {}
533
+ if isinstance(message, dict):
534
+ tool_call_count = len(message.get("tool_calls") or [])
535
+ except Exception:
536
+ tool_call_count = 0
537
+
538
+ log.info(
539
+ "MockRLTrainer proxy returning response with %s tool calls (cid=%s)",
540
+ tool_call_count,
541
+ cid,
542
+ )
543
+ if tool_call_count == 0:
544
+ log.error(
545
+ "MockRLTrainer proxy still missing tool_calls after fallback injection (cid=%s)",
546
+ cid,
547
+ )
548
+ click.echo(
549
+ "[mock-rl] ✗ proxy response missing tool_calls; failing request", err=True
550
+ )
551
+ return JSONResponse(data_typed)
552
+ return JSONResponse(result)
553
+
554
+ if emit_tool:
555
+ # Emit BOTH legacy function_call and modern tool_calls for broad compatibility
556
+ message_payload = {
557
+ "role": "assistant",
558
+ "content": "",
559
+ "function_call": {
560
+ "name": tool_name,
561
+ "arguments": json.dumps({"actions": actions}),
562
+ },
563
+ "tool_calls": [
564
+ {
565
+ "id": f"call_{uuid.uuid4().hex[:8]}",
566
+ "type": "function",
567
+ "function": {
568
+ "name": tool_name,
569
+ "arguments": json.dumps({"actions": actions}),
570
+ },
571
+ }
572
+ ],
573
+ }
574
+ finish_reason = "tool_calls"
575
+ else:
576
+ # Fallback: echo last user content as plain text
577
+ click.echo(
578
+ f"[mock-rl] ! no tool schema supplied; returning text response (cid={cid})",
579
+ err=True,
580
+ )
581
+ log.warning(
582
+ "MockRLTrainer received request without tool schema; responding with text content (cid=%s)",
583
+ cid,
584
+ )
585
+ last_user = next((m.get("content", "") for m in reversed(messages) if m.get("role") == "user"), "")
586
+ text = (last_user or "").strip()
587
+ if len(text) > 160:
588
+ text = text[:160] + "..."
589
+ message_payload = {"role": "assistant", "content": f"MOCK(gpt-5-nano): {text or 'ack'}"}
590
+ finish_reason = "stop"
591
+
592
+ response = {
593
+ "id": f"cmpl_{uuid.uuid4().hex[:12]}",
594
+ "object": "chat.completion",
595
+ "created": int(asyncio.get_event_loop().time()),
596
+ "model": model,
597
+ "choices": [{"index": 0, "message": message_payload, "finish_reason": finish_reason}],
598
+ "usage": {"prompt_tokens": 32, "completion_tokens": 16, "total_tokens": 48},
599
+ "synth": {"cid": correlation},
600
+ }
601
+ if finish_reason == "tool_calls":
602
+ # Type-safe extraction of tool call count
603
+ tc = 0
604
+ try:
605
+ choices = response.get("choices")
606
+ if isinstance(choices, list) and choices:
607
+ first_choice = choices[0]
608
+ if isinstance(first_choice, dict):
609
+ msg = first_choice.get("message")
610
+ if isinstance(msg, dict):
611
+ tool_calls = msg.get("tool_calls")
612
+ if isinstance(tool_calls, list):
613
+ tc = len(tool_calls)
614
+ except Exception:
615
+ pass
616
+ log.debug(
617
+ "MockRLTrainer synthetic response emitting %s tool calls (cid=%s)",
618
+ tc,
619
+ cid,
620
+ )
621
+ assert tc > 0, "MockRLTrainer synthetic response missing tool_calls"
622
+ click.echo(
623
+ f"[mock-rl] → response tool_calls={tc} backend={backend} cid={cid}",
624
+ err=True,
625
+ )
626
+ else:
627
+ click.echo(
628
+ f"[mock-rl] → response finish_reason={finish_reason} backend={backend} cid={cid}",
629
+ err=True,
630
+ )
631
+ return JSONResponse(response)
632
+
633
+ return app
634
+
635
+ async def start(self) -> None:
636
+ import socket
637
+
638
+ import uvicorn
639
+
640
+ def _allocate_port() -> int:
641
+ nonlocal socket
642
+ if self.port:
643
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
644
+ probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
645
+ try:
646
+ probe.bind(("127.0.0.1", self.port))
647
+ return self.port
648
+ except OSError:
649
+ pass
650
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
651
+ probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
652
+ probe.bind(("127.0.0.1", 0))
653
+ self.port = probe.getsockname()[1]
654
+ return self.port
655
+
656
+ retries = 0
657
+ while True:
658
+ selected_port = _allocate_port()
659
+ config = uvicorn.Config(
660
+ self._build_app(),
661
+ host="127.0.0.1",
662
+ port=selected_port,
663
+ log_level="warning",
664
+ )
665
+ self._server = uvicorn.Server(config)
666
+ self._task = asyncio.create_task(self._server.serve())
667
+
668
+ for _ in range(100):
669
+ if getattr(self._server, "started", False):
670
+ break
671
+ if self._task.done():
672
+ break
673
+ await asyncio.sleep(0.05)
674
+
675
+ if getattr(self._server, "started", False):
676
+ try:
677
+ logging.getLogger(__name__).info(
678
+ "MockRLTrainer started on http://127.0.0.1:%s (backend=%s)",
679
+ self.port,
680
+ self.backend,
681
+ )
682
+ click.echo(
683
+ f"[mock-rl] server ready http://127.0.0.1:{self.port} backend={self.backend}",
684
+ err=True,
685
+ )
686
+ except Exception:
687
+ pass
688
+ return
689
+
690
+ # Startup failed; stop server and retry on a new port if possible
691
+ await self.stop()
692
+ if retries >= 5:
693
+ raise RuntimeError("MockRLTrainer failed to start after multiple attempts")
694
+ self.port = 0
695
+ retries += 1
696
+
697
+ async def stop(self) -> None:
698
+ if self._server is not None:
699
+ self._server.should_exit = True
700
+ if self._task is not None:
701
+ with contextlib.suppress(Exception):
702
+ await asyncio.wait_for(self._task, timeout=2.0)
703
+ self._task = None
704
+ self._server = None
705
+ click.echo("[mock-rl] server stopped", err=True)
706
+
707
+ async def _run_smoke_async(
708
+ *,
709
+ task_app_url: str,
710
+ api_key: str | None,
711
+ env_name_opt: str | None,
712
+ policy_name: str,
713
+ model: str,
714
+ inference_url_opt: str | None,
715
+ inference_policy: str | None,
716
+ max_steps: int,
717
+ return_trace: bool,
718
+ use_mock: bool,
719
+ mock_port: int,
720
+ mock_backend: str,
721
+ config_path: Path | None,
722
+ rollouts: int = 1,
723
+ group_size: int = 1,
724
+ batch_size: int | None = None,
725
+ ) -> int:
726
+ # If config is provided, derive defaults (URL/env/model)
727
+ cfg: Any | None = None
728
+ if config_path is not None:
729
+ try:
730
+ from synth_ai.api.train.configs.rl import (
731
+ RLConfig as _RLConfig, # lazy import to avoid heavy deps when unused
732
+ )
733
+ cfg = _RLConfig.from_path(config_path)
734
+ except Exception as exc:
735
+ click.echo(f"Failed to load RL config {config_path}: {exc}", err=True)
736
+ return 2
737
+
738
+ # Prefer explicit CLI --url; only use config services.task_url if URL not provided
739
+ try:
740
+ if not task_app_url and cfg.services and getattr(cfg.services, "task_url", None):
741
+ task_app_url = cfg.services.task_url
742
+ except Exception:
743
+ pass
744
+ # Fill env and model if not explicitly set
745
+ try:
746
+ if not env_name_opt and cfg.rollout and getattr(cfg.rollout, "env_name", None):
747
+ env_name_opt = cfg.rollout.env_name
748
+ except Exception:
749
+ pass
750
+ try:
751
+ if model == "gpt-5-nano":
752
+ # Prefer smoke config model over policy model for smoke tests
753
+ smoke_cfg = getattr(cfg, "smoke", None)
754
+ smoke_model = None
755
+ if smoke_cfg and hasattr(smoke_cfg, "model"):
756
+ smoke_model = smoke_cfg.model
757
+ if smoke_model:
758
+ model = str(smoke_model).strip()
759
+ elif cfg.policy:
760
+ if getattr(cfg.policy, "model_name", None):
761
+ model = str(cfg.policy.model_name).strip()
762
+ elif getattr(cfg.policy, "source", None):
763
+ model = str(cfg.policy.source).strip()
764
+ elif cfg.model and getattr(cfg.model, "source", None):
765
+ model = str(cfg.model.source).strip()
766
+ elif cfg.model and getattr(cfg.model, "base", None):
767
+ model = str(cfg.model.base).strip()
768
+ except Exception:
769
+ pass
770
+
771
+ base = validate_task_app_url(task_app_url)
772
+ mock_backend = (mock_backend or "synthetic").strip().lower()
773
+
774
+ # Discover environment if not provided
775
+ async with TaskAppClient(base_url=base, api_key=api_key) as client:
776
+ # Probe basic info quickly
777
+ try:
778
+ _ = await client.health()
779
+ except Exception:
780
+ click.echo("Auth or connectivity check failed on /health. If this endpoint requires a key, pass --api-key or set ENVIRONMENT_API_KEY.", err=True)
781
+ # Continue; rollout may still clarify the error
782
+
783
+ # Fetch a sample task instance to infer environment name if not provided
784
+ env_name = env_name_opt
785
+ if not env_name:
786
+ try:
787
+ ti = await client.task_info(seeds=[0])
788
+ # task_info returns TaskInfo or list[TaskInfo]; normalize
789
+ info: Any = ti[0] if isinstance(ti, list) else ti
790
+ env_name = getattr(info, "environment", None) or getattr(info, "task", {}).get("name") # type: ignore[attr-defined]
791
+ except Exception:
792
+ env_name = None
793
+ if not env_name:
794
+ click.echo("Could not infer environment name; pass --env-name.", err=True)
795
+ return 2
796
+
797
+ # Build ops: alternating agent/env for max_steps
798
+ ops: list[str] = []
799
+ for _ in range(max_steps):
800
+ ops.append("agent")
801
+ ops.append("env")
802
+
803
+ # Inference URL: user override > preset > local mock > Synth API default
804
+ synth_base = (os.getenv("SYNTH_API_BASE") or os.getenv("SYNTH_BASE_URL") or "https://api.synth.run").rstrip("/")
805
+ # Avoid double '/api' if base already includes it
806
+ if synth_base.endswith("/api"):
807
+ default_infer = f"{synth_base}/inference/v1/chat/completions"
808
+ else:
809
+ default_infer = f"{synth_base}/api/inference/v1/chat/completions"
810
+
811
+ # Helper to execute one or more rollouts and return exit code
812
+ async def __do_rollouts(inference_url_raw: str) -> int:
813
+ successes = 0
814
+ total_steps = 0
815
+ nonzero_returns = 0
816
+ v3_traces = 0
817
+
818
+ # Derive sampling params from config if present
819
+ sampling: dict[str, Any] = {}
820
+ try:
821
+ if cfg and cfg.policy:
822
+ if getattr(cfg.policy, "temperature", None) is not None:
823
+ sampling["temperature"] = cfg.policy.temperature
824
+ if getattr(cfg.policy, "top_p", None) is not None:
825
+ sampling["top_p"] = cfg.policy.top_p
826
+ if getattr(cfg.policy, "max_tokens", None) is not None:
827
+ sampling["max_tokens"] = cfg.policy.max_tokens
828
+ except Exception:
829
+ pass
830
+
831
+ num_outer = batch_size if (batch_size is not None and batch_size > 0) else max(1, int(rollouts))
832
+ for i in range(num_outer):
833
+ for g in range(max(1, int(group_size))):
834
+ if inference_url_raw.startswith("/"):
835
+ inference_url_abs = f"{base}{inference_url_raw}"
836
+ else:
837
+ inference_url_abs = inference_url_raw
838
+ inference_url_norm = normalize_inference_url(inference_url_abs)
839
+ correlation_id = f"smoke-{uuid.uuid4()}"
840
+ inference_url_with_cid = _append_query_param(inference_url_norm, "cid", correlation_id)
841
+
842
+ run_id = correlation_id
843
+ policy_cfg: dict[str, Any] = {
844
+ "model": model,
845
+ "inference_url": inference_url_with_cid,
846
+ }
847
+ if sampling:
848
+ policy_cfg.update(sampling)
849
+
850
+ request = RolloutRequest(
851
+ run_id=run_id,
852
+ env=RolloutEnvSpec(env_name=env_name, config={}, seed=i),
853
+ policy=RolloutPolicySpec(policy_name=policy_name, config=policy_cfg),
854
+ ops=ops,
855
+ record=RolloutRecordConfig(
856
+ trajectories=True,
857
+ logprobs=False,
858
+ value=False,
859
+ return_trace=return_trace,
860
+ trace_format=("structured" if return_trace else "compact"),
861
+ ),
862
+ on_done="reset",
863
+ safety=RolloutSafetyConfig(max_ops=max_steps * 4, max_time_s=900.0),
864
+ training_session_id=None,
865
+ synth_base_url=synth_base,
866
+ mode=RolloutMode.RL,
867
+ )
868
+
869
+ try:
870
+ click.echo(f">> POST /rollout run_id={run_id} env={env_name} policy={policy_name} url={inference_url_with_cid}")
871
+ click.echo(f" ops={ops[:10]}{'...' if len(ops) > 10 else ''}")
872
+ response = await client.rollout(request)
873
+ except Exception as exc:
874
+ click.echo(f"Rollout[{i}:{g}] failed: {type(exc).__name__}: {exc}", err=True)
875
+ import traceback
876
+ click.echo(f"Traceback: {traceback.format_exc()}", err=True)
877
+ continue
878
+
879
+ successes += 1
880
+ try:
881
+ validate_rollout_response_for_rl(response.model_dump())
882
+ except Exception as vexc:
883
+ click.echo(f" ⚠ RL response validation warning: {vexc}", err=True)
884
+
885
+ pm = response.pipeline_metadata or {}
886
+ inferred_url = pm.get("inference_url") if isinstance(pm, dict) else None
887
+ metrics = response.metrics
888
+ if inferred_url:
889
+ click.echo(f" rollout[{i}:{g}] inference_url: {inferred_url}")
890
+ click.echo(f" rollout[{i}:{g}] episodes={metrics.num_episodes} steps={metrics.num_steps} mean_return={metrics.mean_return:.4f}")
891
+
892
+ total_steps += int(metrics.num_steps)
893
+ if (metrics.mean_return or 0.0) != 0.0:
894
+ nonzero_returns += 1
895
+ if response.trace is not None and isinstance(response.trace, dict):
896
+ v3_traces += 1
897
+
898
+ if i == 0 and g == 0:
899
+ try:
900
+ traj0 = response.trajectories[0]
901
+ step_meta_url = None
902
+ for step in traj0.steps:
903
+ info = getattr(step, "info", None) or {}
904
+ meta = info.get("meta") if isinstance(info, dict) else None
905
+ if isinstance(meta, dict) and meta.get("inference_url"):
906
+ step_meta_url = meta.get("inference_url")
907
+ break
908
+ if step_meta_url:
909
+ click.echo(f" step.meta.inference_url: {str(step_meta_url)[:120]}...")
910
+ except Exception:
911
+ pass
912
+
913
+ try:
914
+ try:
915
+ metrics_dump = response.metrics.model_dump()
916
+ except Exception:
917
+ metrics_dump = {
918
+ "episode_returns": getattr(response.metrics, "episode_returns", None),
919
+ "mean_return": getattr(response.metrics, "mean_return", None),
920
+ "num_steps": getattr(response.metrics, "num_steps", None),
921
+ "num_episodes": getattr(response.metrics, "num_episodes", None),
922
+ "outcome_score": getattr(response.metrics, "outcome_score", None),
923
+ "events_score": getattr(response.metrics, "events_score", None),
924
+ }
925
+ click.echo(" reward.info (metrics): " + str(metrics_dump))
926
+
927
+ try:
928
+ traj = response.trajectories[0]
929
+ step_rewards = []
930
+ all_achievements = set()
931
+ for st in getattr(traj, "steps", []) or []:
932
+ try:
933
+ step_rewards.append(getattr(st, "reward", None))
934
+ except Exception:
935
+ step_rewards.append(None)
936
+ # Extract achievements from step info
937
+ try:
938
+ step_info = getattr(st, "info", None)
939
+ if isinstance(step_info, dict):
940
+ achievements_status = step_info.get("achievements_status")
941
+ if isinstance(achievements_status, dict):
942
+ for ach_name, ach_val in achievements_status.items():
943
+ if ach_val:
944
+ all_achievements.add(str(ach_name))
945
+ except Exception:
946
+ pass
947
+ click.echo(" reward.per_step: " + str(step_rewards))
948
+ if all_achievements:
949
+ click.echo(f" achievements: {sorted(all_achievements)}")
950
+ else:
951
+ click.echo(" achievements: none")
952
+ except Exception:
953
+ pass
954
+
955
+ # Extract and display tool calls from v3 trace
956
+ #
957
+ # IMPORTANT: Tool calls are extracted from the structured v3 trace format.
958
+ # The trace must be requested with return_trace=True for this to work.
959
+ #
960
+ # Trace structure:
961
+ # trace.event_history[] - list of events (policy calls, env steps)
962
+ # ├─ event.call_records[] - LLM calls made during this event
963
+ # ├─ call_record.output_tool_calls[] - tool calls from LLM response
964
+ # ├─ tool_call.name - function name (e.g., "interact_many")
965
+ # └─ tool_call.arguments_json - JSON string of arguments
966
+ #
967
+ # This provides visibility into what actions the policy is taking,
968
+ # which is critical for debugging RL training issues.
969
+ tr = response.trace if isinstance(response.trace, dict) else None
970
+ if tr:
971
+ event_history = tr.get("event_history", [])
972
+ tool_call_count = 0
973
+
974
+ # Extract tool calls from event_history call_records
975
+ if event_history and isinstance(event_history, list):
976
+ for event in event_history:
977
+ if not isinstance(event, dict):
978
+ continue
979
+ # Policy events contain call_records with LLM interactions
980
+ call_records = event.get("call_records")
981
+ if call_records and isinstance(call_records, list):
982
+ for call_record in call_records:
983
+ if isinstance(call_record, dict):
984
+ # Extract tool calls from this LLM call
985
+ output_tool_calls = call_record.get("output_tool_calls", [])
986
+ if output_tool_calls and isinstance(output_tool_calls, list):
987
+ for tc in output_tool_calls:
988
+ if isinstance(tc, dict):
989
+ fn_name = tc.get("name", "unknown")
990
+ fn_args = tc.get("arguments_json", "{}")
991
+ # Display tool call with truncated args for readability
992
+ click.echo(f" TOOL_CALL[{tool_call_count}]: {fn_name}({fn_args[:100]}{'...' if len(fn_args) > 100 else ''})")
993
+ tool_call_count += 1
994
+
995
+ if tool_call_count > 0:
996
+ click.echo(f" ✓ {tool_call_count} tool calls executed")
997
+ else:
998
+ # No tool calls found - might indicate:
999
+ # 1. return_trace=False (trace not requested)
1000
+ # 2. Policy didn't make tool calls (unlikely for most RL tasks)
1001
+ # 3. Trace format mismatch (structure changed)
1002
+ click.echo(" ⚠ No tool calls found in trace")
1003
+ else:
1004
+ click.echo(" ⚠ Trace not available")
1005
+ except Exception as e:
1006
+ click.echo(f" trace error: {e}", err=True)
1007
+
1008
+ click.echo("✓ Smoke rollouts complete")
1009
+ denom = num_outer * max(1, int(group_size))
1010
+ click.echo(f" successes={successes}/{denom} total_steps={total_steps} v3_traces={v3_traces}/{denom} nonzero_returns={nonzero_returns}/{denom}")
1011
+
1012
+ if successes == 0:
1013
+ click.echo(" ⚠ All rollouts failed", err=True)
1014
+ return 3
1015
+ if v3_traces < successes:
1016
+ click.echo(" ⚠ Some rollouts missing v3 traces (trace field)", err=True)
1017
+ if total_steps == 0:
1018
+ click.echo(" ⚠ No steps executed; check ops/policy config", err=True)
1019
+
1020
+ return 0
1021
+
1022
+ # Initialize to default; policy/flags may override below
1023
+ inference_url_raw = inference_url_opt or default_infer
1024
+ mock: MockRLTrainer | None = None
1025
+ preset = (inference_policy or "").strip().lower()
1026
+
1027
+ # Respect explicit preset overrides
1028
+ if preset == "mock":
1029
+ use_mock = True
1030
+ elif preset == "gpt-5-nano":
1031
+ if not inference_url_opt:
1032
+ inference_url_raw = default_infer
1033
+ if not model:
1034
+ model = "gpt-5-nano"
1035
+ elif preset == "openai":
1036
+ inference_url_raw = "https://api.openai.com/v1/chat/completions"
1037
+ elif preset == "groq":
1038
+ inference_url_raw = "https://api.groq.com/openai/v1/chat/completions"
1039
+
1040
+ # Start mock proxy only when explicitly requested
1041
+ if use_mock:
1042
+ backend_choice = mock_backend
1043
+ if backend_choice == "openai" and not (
1044
+ os.getenv("SMOKE_OPENAI_API_KEY") or os.getenv("OPENAI_API_KEY")
1045
+ ):
1046
+ click.echo(
1047
+ " ⚠ OPENAI_API_KEY not configured; falling back to synthetic mock.",
1048
+ err=True,
1049
+ )
1050
+ backend_choice = "synthetic"
1051
+ mock = MockRLTrainer(port=mock_port, backend=backend_choice)
1052
+ await mock.start()
1053
+ inference_url_raw = f"http://127.0.0.1:{mock.port}"
1054
+
1055
+ try:
1056
+ result = await __do_rollouts(inference_url_raw)
1057
+ finally:
1058
+ if mock is not None:
1059
+ with contextlib.suppress(Exception):
1060
+ await mock.stop()
1061
+ return result
1062
+ async def _run_train_step(
1063
+ *,
1064
+ task_app_url: str,
1065
+ api_key: str | None,
1066
+ env_name_opt: str | None,
1067
+ policy_name: str,
1068
+ model: str,
1069
+ inference_policy: str | None,
1070
+ inference_url_opt: str | None,
1071
+ max_steps: int,
1072
+ return_trace: bool,
1073
+ use_mock: bool,
1074
+ mock_backend: str,
1075
+ mock_port: int,
1076
+ config_path: Path | None,
1077
+ parallel: int,
1078
+ ) -> int:
1079
+ import time
1080
+ start = time.perf_counter()
1081
+
1082
+ async def one(seed_idx: int) -> dict[str, Any]:
1083
+ t0 = time.perf_counter()
1084
+ try:
1085
+ code = await _run_smoke_async(
1086
+ task_app_url=task_app_url,
1087
+ api_key=api_key,
1088
+ env_name_opt=env_name_opt,
1089
+ policy_name=policy_name,
1090
+ model=model,
1091
+ inference_policy=inference_policy,
1092
+ inference_url_opt=inference_url_opt,
1093
+ max_steps=max_steps,
1094
+ return_trace=return_trace,
1095
+ use_mock=use_mock,
1096
+ mock_backend=mock_backend,
1097
+ mock_port=mock_port,
1098
+ config_path=config_path,
1099
+ rollouts=1,
1100
+ group_size=1,
1101
+ batch_size=None,
1102
+ )
1103
+ wall_ms = (time.perf_counter() - t0) * 1000.0
1104
+ return {"exit": int(code), "wall_ms": wall_ms}
1105
+ except Exception as e:
1106
+ wall_ms = (time.perf_counter() - t0) * 1000.0
1107
+ return {"exit": 99, "wall_ms": wall_ms, "error": f"{type(e).__name__}: {e}"}
1108
+
1109
+ # Launch N rollouts concurrently
1110
+ tasks = [one(i) for i in range(max(1, int(parallel)))]
1111
+ results = await asyncio.gather(*tasks, return_exceptions=False)
1112
+ total_wall_ms = (time.perf_counter() - start) * 1000.0
1113
+
1114
+ # Print summary
1115
+ def _exit_code(result: dict[str, Any]) -> int:
1116
+ value = result.get("exit")
1117
+ if isinstance(value, int | float):
1118
+ return int(value)
1119
+ if isinstance(value, str) and value.strip():
1120
+ try:
1121
+ return int(value.strip())
1122
+ except ValueError:
1123
+ return 1
1124
+ return 1
1125
+
1126
+ successes = sum(1 for r in results if _exit_code(r) == 0)
1127
+ avg_wall = sum(float(r.get("wall_ms", 0.0)) for r in results) / max(len(results), 1)
1128
+ click.echo("✓ Train-step emulation complete")
1129
+ click.echo(f" parallel={parallel} successes={successes}/{len(results)} total_wall_ms={total_wall_ms:.1f} avg_rollout_wall_ms={avg_wall:.1f}")
1130
+
1131
+ # Show brief failure codes to aid diagnosis
1132
+ if successes < len(results):
1133
+ codes: dict[int, int] = {}
1134
+ for r in results:
1135
+ if not isinstance(r, dict):
1136
+ continue
1137
+ c = _exit_code(r)
1138
+ codes[c] = codes.get(c, 0) + 1
1139
+ click.echo(f" failure_codes={codes}")
1140
+
1141
+ return 0 if successes == len(results) else 3
1142
+
1143
+
1144
+ @click.command("smoke")
1145
+ @click.option("--url", "task_app_url", type=str, default=lambda: os.getenv("TASK_APP_URL", "http://localhost:8765"), help="Task app base URL.")
1146
+ @click.option(
1147
+ "--api-key",
1148
+ type=str,
1149
+ default=lambda: os.getenv("ENVIRONMENT_API_KEY", ""),
1150
+ envvar="ENVIRONMENT_API_KEY",
1151
+ help="Environment API key (X-API-Key).",
1152
+ )
1153
+ @click.option("--env-name", type=str, default=None, help="Environment name to roll out (auto-detected if possible).")
1154
+ @click.option("--policy-name", type=str, default="react", help="Policy name to pass to task app.")
1155
+ @click.option("--model", type=str, default="gpt-5-nano", help="Model id to route in inference payload.")
1156
+ @click.option(
1157
+ "--policy",
1158
+ "inference_policy",
1159
+ type=click.Choice(["mock", "gpt-5-nano", "openai", "groq"], case_sensitive=False),
1160
+ default=None,
1161
+ help="Inference route preset (mock, gpt-5-nano via Synth, OpenAI or Groq).",
1162
+ )
1163
+ @click.option("--inference-url", type=str, default=None, help="Override inference URL (default: Synth API chat completions).")
1164
+ @click.option("--max-steps", type=int, default=3, show_default=True, help="Number of agent/env step pairs.")
1165
+ @click.option("--return-trace", is_flag=True, help="Request v3 trace in response if supported.")
1166
+ @click.option("--use-mock/--no-mock", default=True, show_default=True, help="Use local mock inference server (GPT-5-Nano emulation).")
1167
+ @click.option(
1168
+ "--mock-backend",
1169
+ type=click.Choice(["synthetic", "openai"], case_sensitive=False),
1170
+ default="synthetic",
1171
+ show_default=True,
1172
+ help="Mock inference backend: synthetic deterministic tooling or OpenAI passthrough.",
1173
+ )
1174
+ @click.option("--mock-port", type=int, default=0, show_default=True, help="Port for local mock inference server (0 = auto).")
1175
+ @click.option("--config", type=click.Path(exists=True, dir_okay=False, path_type=Path), default=None, help="RL TOML config to derive URL/env/model.")
1176
+ @click.option("--env-file", type=click.Path(exists=True, dir_okay=False, path_type=Path), default=None, help="Path to .env to load before running.")
1177
+ @click.option("--rollouts", type=int, default=1, show_default=True, help="Number of rollouts (seeds 0..N-1).")
1178
+ @click.option("--group-size", type=int, default=1, show_default=True, help="Completions per seed to emulate GRPO grouping.")
1179
+ @click.option("--batch-size", type=int, default=None, help="Alias for rollouts; when set, overrides --rollouts.")
1180
+ @click.option(
1181
+ "--parallel",
1182
+ type=int,
1183
+ default=0,
1184
+ show_default=True,
1185
+ help="Emulate a train step by running this many rollouts concurrently (0 = sequential).",
1186
+ )
1187
+ def command(
1188
+ task_app_url: str,
1189
+ api_key: str,
1190
+ env_name: str | None,
1191
+ policy_name: str,
1192
+ model: str,
1193
+ inference_policy: str | None,
1194
+ inference_url: str | None,
1195
+ max_steps: int,
1196
+ return_trace: bool,
1197
+ use_mock: bool,
1198
+ mock_backend: str,
1199
+ mock_port: int,
1200
+ config: Path | None,
1201
+ env_file: Path | None,
1202
+ rollouts: int,
1203
+ group_size: int,
1204
+ batch_size: int | None,
1205
+ parallel: int,
1206
+ ) -> None:
1207
+ """Smoke-test a Task App by emulating a trainer rollout using GPT-5-Nano.
1208
+
1209
+ This command posts a minimal RL rollout to the task app, with a valid
1210
+ OpenAI-compatible inference URL including a trace correlation id, and
1211
+ validates that the response contains the fields required by the RL trainer
1212
+ (e.g. pipeline_metadata.inference_url and per-step info.meta.inference_url).
1213
+
1214
+ If --config is provided, loads settings from the [smoke] section in the TOML file.
1215
+ CLI arguments override TOML values.
1216
+ """
1217
+
1218
+ # Load [smoke] section from TOML if config is provided
1219
+ smoke_config = _load_smoke_config(config)
1220
+
1221
+ # Track background processes for cleanup
1222
+ background_procs: list[Any] = []
1223
+
1224
+ try:
1225
+ # Auto-start sqld if configured
1226
+ if smoke_config.get("sqld_auto_start"):
1227
+ sqld_db_path = smoke_config.get("sqld_db_path", "./traces/local.db")
1228
+ sqld_hrana_port = smoke_config.get("sqld_hrana_port", 8080)
1229
+ sqld_http_port = smoke_config.get("sqld_http_port", 8081)
1230
+
1231
+ sqld_proc = _start_sqld_server(
1232
+ db_path=sqld_db_path,
1233
+ hrana_port=sqld_hrana_port,
1234
+ http_port=sqld_http_port,
1235
+ )
1236
+ if sqld_proc:
1237
+ background_procs.append(("sqld", sqld_proc))
1238
+
1239
+ # Auto-start task app if configured
1240
+ task_app_override_url = None
1241
+ if smoke_config.get("task_app_name"):
1242
+ task_app_name = smoke_config["task_app_name"]
1243
+ task_app_port = smoke_config.get("task_app_port", 8765)
1244
+ task_app_env_file = smoke_config.get("task_app_env_file")
1245
+ task_app_force = smoke_config.get("task_app_force", True)
1246
+
1247
+ task_app_proc, task_app_url = _start_task_app_server(
1248
+ task_app_name=task_app_name,
1249
+ port=task_app_port,
1250
+ env_file=task_app_env_file,
1251
+ force=task_app_force,
1252
+ )
1253
+ background_procs.append(("task_app", task_app_proc))
1254
+ task_app_override_url = task_app_url
1255
+ click.echo(f"[smoke] Task app started, will use URL: {task_app_url}", err=True)
1256
+ except Exception as exc:
1257
+ # Cleanup any processes that did start
1258
+ for proc_name, proc in background_procs:
1259
+ if proc and proc.poll() is None:
1260
+ click.echo(f"[smoke] Cleaning up {proc_name}...", err=True)
1261
+ proc.terminate()
1262
+ try:
1263
+ proc.wait(timeout=3)
1264
+ except Exception:
1265
+ proc.kill()
1266
+
1267
+ click.echo(f"[smoke] ERROR: Auto-start failed: {exc}", err=True)
1268
+ raise click.ClickException(f"Auto-start failed: {exc}") from exc
1269
+
1270
+ # Apply TOML defaults (CLI args take precedence)
1271
+ # Override task_url with auto-started task app URL if applicable
1272
+ if task_app_override_url:
1273
+ task_app_url = task_app_override_url
1274
+ # For string/int args: use TOML value if CLI value matches the default
1275
+ ctx = click.get_current_context()
1276
+
1277
+ # Helper to check if a CLI param was explicitly provided or is using default
1278
+ def use_toml_default(param_name: str, cli_value: Any, toml_key: str) -> Any:
1279
+ """Use TOML value if CLI param is at its default, otherwise use CLI value."""
1280
+ if not smoke_config or toml_key not in smoke_config:
1281
+ return cli_value
1282
+
1283
+ param = next((p for p in ctx.command.params if p.name == param_name), None)
1284
+ if not param:
1285
+ return cli_value
1286
+
1287
+ # Check if value was explicitly provided (not default)
1288
+ # If it matches the default, use TOML value
1289
+ param_default = param.default() if callable(param.default) else param.default
1290
+ if cli_value == param_default:
1291
+ toml_value = smoke_config[toml_key]
1292
+ click.echo(f"[smoke] Using {toml_key}={toml_value} from config", err=True)
1293
+ return toml_value
1294
+
1295
+ return cli_value
1296
+
1297
+ # Apply TOML defaults
1298
+ task_app_url = use_toml_default("task_app_url", task_app_url, "task_url")
1299
+ env_name = use_toml_default("env_name", env_name, "env_name")
1300
+ policy_name = use_toml_default("policy_name", policy_name, "policy_name")
1301
+ model = use_toml_default("model", model, "model")
1302
+ inference_policy = use_toml_default("inference_policy", inference_policy, "policy")
1303
+ inference_url = use_toml_default("inference_url", inference_url, "inference_url")
1304
+ max_steps = use_toml_default("max_steps", max_steps, "max_steps")
1305
+ return_trace = use_toml_default("return_trace", return_trace, "return_trace")
1306
+ use_mock = use_toml_default("use_mock", use_mock, "use_mock")
1307
+ mock_backend = use_toml_default("mock_backend", mock_backend, "mock_backend")
1308
+ mock_port = use_toml_default("mock_port", mock_port, "mock_port")
1309
+ api_key = use_toml_default("api_key", api_key, "api_key")
1310
+
1311
+ # Auto-configure tracing to avoid interactive prompts
1312
+ try:
1313
+ os.environ.setdefault("CI", "true")
1314
+ os.environ.setdefault("SYNTH_TRACING_AUTO_YES", "1")
1315
+ # Derive a default traces directory relative to CWD
1316
+ traces_dir = os.environ.get("SYNTH_TRACES_DIR")
1317
+ if not traces_dir:
1318
+ traces_dir = str((Path.cwd() / "traces" / "v3").resolve())
1319
+ os.environ["SYNTH_TRACES_DIR"] = traces_dir
1320
+ with contextlib.suppress(Exception):
1321
+ Path(traces_dir).mkdir(parents=True, exist_ok=True)
1322
+ _ensure_local_libsql()
1323
+ # Prefer a libsql/turso/sqld URL when provided to enable concurrent writes
1324
+ libsql_url = (
1325
+ os.getenv("TRACING_DB_URL")
1326
+ or os.getenv("LIBSQL_URL")
1327
+ or os.getenv("TURSO_DATABASE_URL")
1328
+ or os.getenv("LIBSQL_HTTP_URL")
1329
+ )
1330
+ if libsql_url:
1331
+ os.environ.setdefault("LIBSQL_URL", libsql_url)
1332
+
1333
+ auth_hint = (
1334
+ os.getenv("TRACING_DB_AUTH_TOKEN")
1335
+ or os.getenv("LIBSQL_AUTH_TOKEN")
1336
+ or os.getenv("TURSO_AUTH_TOKEN")
1337
+ )
1338
+ if auth_hint:
1339
+ os.environ.setdefault("LIBSQL_AUTH_TOKEN", auth_hint)
1340
+
1341
+ resolved_url, resolved_token = resolve_trace_db_settings()
1342
+ os.environ.setdefault("SYNTH_TRACES_DB", resolved_url)
1343
+ if resolved_token and not (
1344
+ os.getenv("LIBSQL_AUTH_TOKEN") or os.getenv("TURSO_AUTH_TOKEN")
1345
+ ):
1346
+ os.environ["LIBSQL_AUTH_TOKEN"] = resolved_token
1347
+
1348
+ _refresh_tracing_config()
1349
+ except Exception:
1350
+ pass
1351
+
1352
+ # Load env file(s) before resolving API key
1353
+ try:
1354
+ # Explicit --env-file takes precedence
1355
+ if env_file is not None:
1356
+ try:
1357
+ from dotenv import load_dotenv as _ld
1358
+ _ld(env_file, override=False)
1359
+ except Exception:
1360
+ pass
1361
+ else:
1362
+ # Best-effort auto-discovery from CWD
1363
+ try:
1364
+ from dotenv import find_dotenv as _fd
1365
+ from dotenv import load_dotenv as _ld
1366
+ _ld(_fd(usecwd=True), override=False)
1367
+ except Exception:
1368
+ pass
1369
+
1370
+ # If api_key not passed, try to read from env now
1371
+ if not api_key:
1372
+ api_key = os.getenv("ENVIRONMENT_API_KEY", "")
1373
+ except Exception:
1374
+ pass
1375
+
1376
+ try:
1377
+ if parallel and parallel > 0:
1378
+ exit_code = asyncio.run(
1379
+ _run_train_step(
1380
+ task_app_url=task_app_url,
1381
+ api_key=(api_key or None),
1382
+ env_name_opt=env_name,
1383
+ policy_name=policy_name,
1384
+ model=model,
1385
+ inference_policy=inference_policy,
1386
+ inference_url_opt=inference_url,
1387
+ max_steps=max_steps,
1388
+ return_trace=return_trace,
1389
+ use_mock=use_mock,
1390
+ mock_backend=mock_backend,
1391
+ mock_port=mock_port,
1392
+ config_path=config,
1393
+ parallel=parallel,
1394
+ )
1395
+ )
1396
+ else:
1397
+ exit_code = asyncio.run(
1398
+ _run_smoke_async(
1399
+ task_app_url=task_app_url,
1400
+ api_key=(api_key or None),
1401
+ env_name_opt=env_name,
1402
+ policy_name=policy_name,
1403
+ model=model,
1404
+ inference_policy=inference_policy,
1405
+ inference_url_opt=inference_url,
1406
+ max_steps=max_steps,
1407
+ return_trace=return_trace,
1408
+ use_mock=use_mock,
1409
+ mock_backend=mock_backend,
1410
+ mock_port=mock_port,
1411
+ config_path=config,
1412
+ rollouts=rollouts,
1413
+ group_size=group_size,
1414
+ batch_size=batch_size,
1415
+ )
1416
+ )
1417
+ except KeyboardInterrupt:
1418
+ click.echo("Interrupted", err=True)
1419
+ sys.exit(130)
1420
+ finally:
1421
+ # Cleanup background processes
1422
+ for proc_name, proc in background_procs:
1423
+ if proc and proc.poll() is None:
1424
+ click.echo(f"[smoke] Stopping {proc_name}...", err=True)
1425
+ proc.terminate()
1426
+ try:
1427
+ proc.wait(timeout=5)
1428
+ except Exception:
1429
+ proc.kill()
1430
+ if background_procs:
1431
+ click.echo("[smoke] Background services stopped", err=True)
1432
+
1433
+ sys.exit(exit_code)
1434
+
1435
+
1436
+ def register(cli: click.Group) -> None:
1437
+ cli.add_command(command)