synth-ai 0.2.14__py3-none-any.whl → 0.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (1091) hide show
  1. synth_ai/__init__.py +19 -40
  2. synth_ai/__main__.py +30 -3
  3. synth_ai/cli/__init__.py +105 -70
  4. synth_ai/cli/__main__.py +42 -0
  5. synth_ai/cli/_internal/__init__.py +5 -0
  6. synth_ai/cli/_internal/modal_wrapper.py +31 -0
  7. synth_ai/cli/_internal/storage.py +20 -0
  8. synth_ai/cli/_internal/typer_patch.py +47 -0
  9. synth_ai/cli/_internal/validate_task_app.py +29 -0
  10. synth_ai/cli/agents/__init__.py +17 -0
  11. synth_ai/cli/agents/claude.py +77 -0
  12. synth_ai/cli/agents/codex.py +265 -0
  13. synth_ai/cli/agents/opencode.py +253 -0
  14. synth_ai/cli/commands/__init__.py +18 -0
  15. synth_ai/cli/commands/artifacts/__init__.py +13 -0
  16. synth_ai/cli/commands/artifacts/client.py +119 -0
  17. synth_ai/cli/commands/artifacts/config.py +57 -0
  18. synth_ai/cli/commands/artifacts/core.py +24 -0
  19. synth_ai/cli/commands/artifacts/download.py +188 -0
  20. synth_ai/cli/commands/artifacts/export.py +186 -0
  21. synth_ai/cli/commands/artifacts/list.py +156 -0
  22. synth_ai/cli/commands/artifacts/parsing.py +250 -0
  23. synth_ai/cli/commands/artifacts/show.py +336 -0
  24. synth_ai/cli/commands/baseline/__init__.py +12 -0
  25. synth_ai/cli/commands/baseline/core.py +636 -0
  26. synth_ai/cli/commands/baseline/list.py +94 -0
  27. synth_ai/cli/commands/demo/__init__.py +3 -0
  28. synth_ai/cli/commands/demo/core.py +153 -0
  29. synth_ai/cli/commands/eval/__init__.py +19 -0
  30. synth_ai/cli/commands/eval/core.py +1113 -0
  31. synth_ai/cli/commands/eval/errors.py +81 -0
  32. synth_ai/cli/commands/eval/validation.py +133 -0
  33. synth_ai/cli/commands/filter/__init__.py +12 -0
  34. synth_ai/cli/commands/filter/core.py +424 -0
  35. synth_ai/cli/commands/filter/errors.py +55 -0
  36. synth_ai/cli/commands/filter/validation.py +77 -0
  37. synth_ai/cli/commands/help/__init__.py +185 -0
  38. synth_ai/cli/commands/help/core.py +72 -0
  39. synth_ai/cli/commands/scan/__init__.py +19 -0
  40. synth_ai/cli/commands/scan/cloudflare_scanner.py +403 -0
  41. synth_ai/cli/commands/scan/core.py +344 -0
  42. synth_ai/cli/commands/scan/health_checker.py +242 -0
  43. synth_ai/cli/commands/scan/local_scanner.py +278 -0
  44. synth_ai/cli/commands/scan/models.py +83 -0
  45. synth_ai/cli/commands/smoke/__init__.py +7 -0
  46. synth_ai/cli/commands/smoke/core.py +1438 -0
  47. synth_ai/cli/commands/status/__init__.py +66 -0
  48. synth_ai/cli/commands/status/client.py +192 -0
  49. synth_ai/cli/commands/status/config.py +92 -0
  50. synth_ai/cli/commands/status/errors.py +20 -0
  51. synth_ai/cli/commands/status/formatters.py +164 -0
  52. synth_ai/cli/commands/status/subcommands/__init__.py +9 -0
  53. synth_ai/cli/commands/status/subcommands/files.py +79 -0
  54. synth_ai/cli/commands/status/subcommands/jobs.py +334 -0
  55. synth_ai/cli/commands/status/subcommands/models.py +79 -0
  56. synth_ai/cli/commands/status/subcommands/pricing.py +23 -0
  57. synth_ai/cli/commands/status/subcommands/runs.py +81 -0
  58. synth_ai/cli/commands/status/subcommands/session.py +182 -0
  59. synth_ai/cli/commands/status/subcommands/summary.py +47 -0
  60. synth_ai/cli/commands/status/subcommands/usage.py +203 -0
  61. synth_ai/cli/commands/status/utils.py +114 -0
  62. synth_ai/cli/commands/train/__init__.py +53 -0
  63. synth_ai/cli/commands/train/core.py +22 -0
  64. synth_ai/cli/commands/train/errors.py +117 -0
  65. synth_ai/cli/commands/train/judge_schemas.py +201 -0
  66. synth_ai/cli/commands/train/judge_validation.py +305 -0
  67. synth_ai/cli/commands/train/prompt_learning_validation.py +633 -0
  68. synth_ai/cli/commands/train/validation.py +392 -0
  69. synth_ai/cli/demo_apps/__init__.py +10 -0
  70. synth_ai/cli/demo_apps/core/__init__.py +28 -0
  71. synth_ai/cli/demo_apps/core/cli.py +1735 -0
  72. synth_ai/cli/demo_apps/crafter/crafter_fft_4b.toml +55 -0
  73. synth_ai/cli/demo_apps/crafter/grpo_crafter_task_app.py +186 -0
  74. synth_ai/cli/demo_apps/crafter/rl_from_base_qwen4b.toml +74 -0
  75. synth_ai/cli/demo_apps/demo_registry.py +176 -0
  76. synth_ai/cli/demo_apps/demo_task_apps/core.py +440 -0
  77. synth_ai/cli/demo_apps/demo_task_apps/crafter/__init__.py +1 -0
  78. synth_ai/cli/demo_apps/demo_task_apps/crafter/grpo_crafter_task_app.py +185 -0
  79. synth_ai/cli/demo_apps/demo_task_apps/math/modal_task_app.py +742 -0
  80. synth_ai/cli/demo_apps/demo_task_apps/math/task_app_entry.py +39 -0
  81. synth_ai/cli/demo_apps/math/__init__.py +1 -0
  82. synth_ai/cli/demo_apps/math/_common.py +16 -0
  83. synth_ai/cli/demo_apps/math/app.py +38 -0
  84. synth_ai/cli/demo_apps/math/config.toml +76 -0
  85. synth_ai/cli/demo_apps/math/deploy_modal.py +54 -0
  86. synth_ai/cli/demo_apps/math/modal_task_app.py +702 -0
  87. synth_ai/cli/demo_apps/math/task_app_entry.py +53 -0
  88. synth_ai/cli/demo_apps/mipro/main.py +271 -0
  89. synth_ai/cli/demo_apps/mipro/task_app.py +933 -0
  90. synth_ai/cli/demo_apps/mipro/train_cfg.toml +92 -0
  91. synth_ai/cli/demos/__init__.py +12 -0
  92. synth_ai/cli/demos/demo.py +32 -0
  93. synth_ai/cli/demos/rl_demo.py +254 -0
  94. synth_ai/cli/deploy.py +216 -0
  95. synth_ai/cli/infra/__init__.py +14 -0
  96. synth_ai/cli/infra/balance.py +216 -0
  97. synth_ai/cli/infra/mcp.py +35 -0
  98. synth_ai/cli/infra/modal_app.py +36 -0
  99. synth_ai/cli/infra/setup.py +69 -0
  100. synth_ai/cli/infra/status.py +16 -0
  101. synth_ai/cli/infra/turso.py +77 -0
  102. synth_ai/cli/lib/__init__.py +10 -0
  103. synth_ai/cli/lib/agents.py +76 -0
  104. synth_ai/cli/lib/apps/modal_app.py +101 -0
  105. synth_ai/cli/lib/apps/task_app.py +643 -0
  106. synth_ai/cli/lib/bin.py +39 -0
  107. synth_ai/cli/lib/env.py +375 -0
  108. synth_ai/cli/lib/errors.py +85 -0
  109. synth_ai/cli/lib/modal.py +315 -0
  110. synth_ai/cli/lib/plotting.py +126 -0
  111. synth_ai/cli/lib/prompt_args.py +39 -0
  112. synth_ai/cli/lib/prompts.py +284 -0
  113. synth_ai/cli/lib/sqld.py +122 -0
  114. synth_ai/cli/lib/task_app_discovery.py +884 -0
  115. synth_ai/cli/lib/task_app_env.py +295 -0
  116. synth_ai/cli/lib/train_cfgs.py +300 -0
  117. synth_ai/cli/lib/tunnel_records.py +207 -0
  118. synth_ai/cli/local/__init__.py +14 -0
  119. synth_ai/cli/local/experiment_queue/__init__.py +72 -0
  120. synth_ai/cli/local/experiment_queue/api_schemas.py +221 -0
  121. synth_ai/cli/local/experiment_queue/celery_app.py +208 -0
  122. synth_ai/cli/local/experiment_queue/config.py +128 -0
  123. synth_ai/cli/local/experiment_queue/config_utils.py +272 -0
  124. synth_ai/cli/local/experiment_queue/database.py +175 -0
  125. synth_ai/cli/local/experiment_queue/dispatcher.py +119 -0
  126. synth_ai/cli/local/experiment_queue/models.py +231 -0
  127. synth_ai/cli/local/experiment_queue/progress_info.py +160 -0
  128. synth_ai/cli/local/experiment_queue/results.py +373 -0
  129. synth_ai/cli/local/experiment_queue/schemas.py +131 -0
  130. synth_ai/cli/local/experiment_queue/service.py +344 -0
  131. synth_ai/cli/local/experiment_queue/status.py +372 -0
  132. synth_ai/cli/local/experiment_queue/status_tracker.py +360 -0
  133. synth_ai/cli/local/experiment_queue/tasks.py +1984 -0
  134. synth_ai/cli/local/experiment_queue/trace_storage.py +65 -0
  135. synth_ai/cli/local/experiment_queue/validation.py +157 -0
  136. synth_ai/cli/local/session/__init__.py +92 -0
  137. synth_ai/cli/local/session/client.py +383 -0
  138. synth_ai/cli/local/session/constants.py +63 -0
  139. synth_ai/cli/local/session/exceptions.py +105 -0
  140. synth_ai/cli/local/session/manager.py +139 -0
  141. synth_ai/cli/local/session/models.py +89 -0
  142. synth_ai/cli/local/session/query.py +110 -0
  143. synth_ai/cli/root.py +30 -6
  144. synth_ai/cli/task_apps/__init__.py +26 -0
  145. synth_ai/cli/task_apps/commands.py +3153 -0
  146. synth_ai/cli/task_apps/deploy.py +7 -0
  147. synth_ai/cli/task_apps/list.py +26 -0
  148. synth_ai/cli/task_apps/main.py +36 -0
  149. synth_ai/cli/task_apps/modal_serve.py +11 -0
  150. synth_ai/cli/task_apps/serve.py +11 -0
  151. synth_ai/cli/training/__init__.py +8 -0
  152. synth_ai/cli/training/train.py +5 -0
  153. synth_ai/cli/training/train_cfg.py +34 -0
  154. synth_ai/cli/training/watch.py +506 -0
  155. synth_ai/cli/turso.py +34 -55
  156. synth_ai/cli/usage.py +159 -0
  157. synth_ai/cli/utils/__init__.py +8 -0
  158. synth_ai/cli/utils/experiments.py +235 -0
  159. synth_ai/cli/utils/queue.py +504 -0
  160. synth_ai/cli/utils/recent.py +133 -0
  161. synth_ai/cli/utils/traces.py +164 -0
  162. synth_ai/contracts/__init__.py +67 -0
  163. synth_ai/core/__init__.py +100 -0
  164. synth_ai/core/_utils/__init__.py +54 -0
  165. synth_ai/core/_utils/base_url.py +10 -0
  166. synth_ai/core/_utils/http.py +10 -0
  167. synth_ai/core/_utils/prompts.py +14 -0
  168. synth_ai/core/_utils/task_app_state.py +12 -0
  169. synth_ai/core/_utils/user_config.py +10 -0
  170. synth_ai/core/apps/common.py +116 -0
  171. synth_ai/core/auth.py +95 -0
  172. synth_ai/core/cfgs.py +240 -0
  173. synth_ai/core/config/__init__.py +16 -0
  174. synth_ai/core/config/base.py +168 -0
  175. synth_ai/core/config/resolver.py +89 -0
  176. synth_ai/core/env.py +220 -0
  177. synth_ai/core/errors.py +126 -0
  178. synth_ai/core/http.py +230 -0
  179. synth_ai/core/integrations/__init__.py +11 -0
  180. synth_ai/core/integrations/cloudflare.py +1710 -0
  181. synth_ai/core/integrations/mcp/__init__.py +6 -0
  182. synth_ai/core/integrations/mcp/__main__.py +8 -0
  183. synth_ai/core/integrations/mcp/claude.py +36 -0
  184. synth_ai/core/integrations/mcp/main.py +254 -0
  185. synth_ai/core/integrations/mcp/setup.py +100 -0
  186. synth_ai/core/integrations/modal.py +277 -0
  187. synth_ai/core/json.py +72 -0
  188. synth_ai/core/log_filter.py +99 -0
  189. synth_ai/core/logging.py +82 -0
  190. synth_ai/core/paths.py +107 -0
  191. synth_ai/core/pricing.py +109 -0
  192. synth_ai/core/process.py +233 -0
  193. synth_ai/core/ssl.py +25 -0
  194. synth_ai/core/storage/__init__.py +71 -0
  195. synth_ai/core/task_app_state.py +318 -0
  196. synth_ai/core/telemetry.py +282 -0
  197. synth_ai/core/tracing_v3/__init__.py +99 -0
  198. synth_ai/core/tracing_v3/abstractions.py +302 -0
  199. synth_ai/core/tracing_v3/config.py +229 -0
  200. synth_ai/core/tracing_v3/constants.py +21 -0
  201. synth_ai/core/tracing_v3/db_config.py +182 -0
  202. synth_ai/core/tracing_v3/decorators.py +401 -0
  203. synth_ai/core/tracing_v3/llm_call_record_helpers.py +437 -0
  204. synth_ai/core/tracing_v3/migration_helper.py +119 -0
  205. synth_ai/core/tracing_v3/session_tracer.py +542 -0
  206. synth_ai/core/tracing_v3/storage/base.py +211 -0
  207. synth_ai/core/tracing_v3/storage/config.py +109 -0
  208. synth_ai/core/tracing_v3/storage/factory.py +39 -0
  209. synth_ai/core/tracing_v3/trace_utils.py +326 -0
  210. synth_ai/core/tracing_v3/turso/daemon.py +278 -0
  211. synth_ai/core/tracing_v3/turso/models.py +470 -0
  212. synth_ai/core/tracing_v3/turso/native_manager.py +1385 -0
  213. synth_ai/core/tracing_v3/utils.py +108 -0
  214. synth_ai/core/urls.py +18 -0
  215. synth_ai/core/user_config.py +137 -0
  216. synth_ai/core/uvicorn.py +222 -0
  217. synth_ai/data/__init__.py +110 -0
  218. synth_ai/data/enums.py +141 -0
  219. synth_ai/data/rewards.py +152 -0
  220. synth_ai/data/specs.py +36 -0
  221. synth_ai/data/traces.py +35 -0
  222. synth_ai/products/__init__.py +6 -0
  223. synth_ai/products/graph_evolve/__init__.py +46 -0
  224. synth_ai/products/graph_evolve/client.py +226 -0
  225. synth_ai/products/graph_evolve/config.py +591 -0
  226. synth_ai/products/graph_evolve/converters/__init__.py +42 -0
  227. synth_ai/products/graph_evolve/converters/openai_sft.py +484 -0
  228. synth_ai/products/graph_evolve/examples/hotpotqa/config.toml +109 -0
  229. synth_ai/products/graph_evolve/run.py +222 -0
  230. synth_ai/sdk/__init__.py +119 -0
  231. synth_ai/sdk/api/__init__.py +1 -0
  232. synth_ai/sdk/api/models/supported.py +514 -0
  233. synth_ai/sdk/api/research_agent/__init__.py +86 -0
  234. synth_ai/sdk/api/research_agent/cli.py +428 -0
  235. synth_ai/sdk/api/research_agent/config.py +357 -0
  236. synth_ai/sdk/api/research_agent/job.py +717 -0
  237. synth_ai/sdk/api/train/__init__.py +85 -0
  238. synth_ai/sdk/api/train/builders.py +895 -0
  239. synth_ai/sdk/api/train/cli.py +2188 -0
  240. synth_ai/sdk/api/train/config_finder.py +267 -0
  241. synth_ai/sdk/api/train/configs/__init__.py +65 -0
  242. synth_ai/sdk/api/train/configs/prompt_learning.py +1706 -0
  243. synth_ai/sdk/api/train/configs/rl.py +188 -0
  244. synth_ai/sdk/api/train/configs/sft.py +99 -0
  245. synth_ai/sdk/api/train/configs/shared.py +81 -0
  246. synth_ai/sdk/api/train/context_learning.py +312 -0
  247. synth_ai/sdk/api/train/env_resolver.py +418 -0
  248. synth_ai/sdk/api/train/graph_validators.py +216 -0
  249. synth_ai/sdk/api/train/graphgen.py +984 -0
  250. synth_ai/sdk/api/train/graphgen_models.py +823 -0
  251. synth_ai/sdk/api/train/graphgen_validators.py +109 -0
  252. synth_ai/sdk/api/train/pollers.py +124 -0
  253. synth_ai/sdk/api/train/progress/__init__.py +97 -0
  254. synth_ai/sdk/api/train/progress/dataclasses.py +569 -0
  255. synth_ai/sdk/api/train/progress/events.py +326 -0
  256. synth_ai/sdk/api/train/progress/results.py +428 -0
  257. synth_ai/sdk/api/train/progress/tracker.py +641 -0
  258. synth_ai/sdk/api/train/prompt_learning.py +470 -0
  259. synth_ai/sdk/api/train/rl.py +442 -0
  260. synth_ai/sdk/api/train/sft.py +396 -0
  261. synth_ai/sdk/api/train/summary.py +522 -0
  262. synth_ai/sdk/api/train/supported_algos.py +147 -0
  263. synth_ai/sdk/api/train/task_app.py +331 -0
  264. synth_ai/sdk/api/train/utils.py +279 -0
  265. synth_ai/sdk/api/train/validators.py +2424 -0
  266. synth_ai/sdk/baseline/__init__.py +25 -0
  267. synth_ai/sdk/baseline/config.py +209 -0
  268. synth_ai/sdk/baseline/discovery.py +216 -0
  269. synth_ai/sdk/baseline/execution.py +154 -0
  270. synth_ai/sdk/graphs/__init__.py +15 -0
  271. synth_ai/sdk/graphs/completions.py +570 -0
  272. synth_ai/sdk/inference/__init__.py +6 -0
  273. synth_ai/sdk/inference/client.py +128 -0
  274. synth_ai/sdk/jobs/__init__.py +16 -0
  275. synth_ai/sdk/jobs/client.py +371 -0
  276. synth_ai/sdk/judging/__init__.py +15 -0
  277. synth_ai/sdk/judging/base.py +24 -0
  278. synth_ai/sdk/judging/client.py +191 -0
  279. synth_ai/sdk/judging/schemas.py +222 -0
  280. synth_ai/sdk/learning/__init__.py +69 -0
  281. synth_ai/sdk/learning/client.py +240 -0
  282. synth_ai/sdk/learning/ft_client.py +7 -0
  283. synth_ai/sdk/learning/health.py +49 -0
  284. synth_ai/sdk/learning/jobs.py +202 -0
  285. synth_ai/sdk/learning/prompt_extraction.py +334 -0
  286. synth_ai/sdk/learning/prompt_learning_client.py +455 -0
  287. synth_ai/sdk/learning/prompt_learning_types.py +185 -0
  288. synth_ai/sdk/learning/rl/client.py +268 -0
  289. synth_ai/sdk/learning/rl/contracts.py +27 -0
  290. synth_ai/sdk/learning/rl/env_keys.py +166 -0
  291. synth_ai/sdk/learning/rl/secrets.py +13 -0
  292. synth_ai/sdk/learning/sft/client.py +95 -0
  293. synth_ai/sdk/learning/sft/config.py +270 -0
  294. synth_ai/sdk/learning/sft/data.py +698 -0
  295. synth_ai/sdk/learning/validators.py +52 -0
  296. synth_ai/sdk/research_agent/__init__.py +34 -0
  297. synth_ai/sdk/research_agent/container_builder.py +328 -0
  298. synth_ai/sdk/research_agent/container_spec.py +198 -0
  299. synth_ai/sdk/research_agent/defaults.py +34 -0
  300. synth_ai/sdk/research_agent/results_collector.py +69 -0
  301. synth_ai/sdk/specs/__init__.py +46 -0
  302. synth_ai/sdk/specs/dataclasses.py +149 -0
  303. synth_ai/sdk/specs/loader.py +144 -0
  304. synth_ai/sdk/specs/serializer.py +199 -0
  305. synth_ai/sdk/specs/validation.py +250 -0
  306. synth_ai/sdk/streaming/__init__.py +35 -0
  307. synth_ai/sdk/streaming/config.py +94 -0
  308. synth_ai/sdk/streaming/handlers.py +1997 -0
  309. synth_ai/sdk/streaming/streamer.py +704 -0
  310. synth_ai/sdk/streaming/types.py +112 -0
  311. synth_ai/sdk/task/__init__.py +151 -0
  312. synth_ai/sdk/task/apps/__init__.py +133 -0
  313. synth_ai/sdk/task/config.py +261 -0
  314. synth_ai/sdk/task/contracts.py +298 -0
  315. synth_ai/sdk/task/datasets.py +108 -0
  316. synth_ai/sdk/task/in_process.py +1190 -0
  317. synth_ai/sdk/task/in_process_runner.py +309 -0
  318. synth_ai/sdk/task/inference_api.py +299 -0
  319. synth_ai/sdk/task/proxy.py +287 -0
  320. synth_ai/sdk/task/rubrics/__init__.py +55 -0
  321. synth_ai/sdk/task/rubrics/loaders.py +156 -0
  322. synth_ai/sdk/task/rubrics.py +219 -0
  323. synth_ai/sdk/task/server.py +580 -0
  324. synth_ai/sdk/task/trace_correlation_helpers.py +506 -0
  325. synth_ai/sdk/task/tracing_utils.py +95 -0
  326. synth_ai/sdk/task/validators.py +456 -0
  327. synth_ai/sdk/tracing/__init__.py +39 -0
  328. synth_ai/sdk/training/__init__.py +102 -0
  329. synth_ai/sdk/usage/__init__.py +37 -0
  330. synth_ai/sdk/usage/client.py +171 -0
  331. synth_ai/sdk/usage/models.py +261 -0
  332. synth_ai/utils/__init__.py +213 -0
  333. synth_ai-0.4.1.dist-info/METADATA +195 -0
  334. synth_ai-0.4.1.dist-info/RECORD +379 -0
  335. synth_ai-0.4.1.dist-info/top_level.txt +1 -0
  336. examples/__init__.py +0 -16
  337. examples/analyze_semantic_words.sh +0 -17
  338. examples/crafter_debug_render.py +0 -186
  339. examples/dev/qwen3_32b_qlora_4xh100.toml +0 -40
  340. examples/multi_step/configs/README_verilog_rl.md +0 -77
  341. examples/multi_step/configs/VERILOG_REWARDS.md +0 -90
  342. examples/multi_step/configs/VERILOG_RL_CHECKLIST.md +0 -183
  343. examples/multi_step/configs/crafter_eval_synth_qwen4b.toml +0 -35
  344. examples/multi_step/configs/crafter_eval_text_only_groq_qwen32b.toml +0 -36
  345. examples/multi_step/configs/crafter_rl_outcome.toml +0 -74
  346. examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +0 -187
  347. examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +0 -83
  348. examples/multi_step/configs/crafter_rl_stepwise_simple.toml +0 -78
  349. examples/multi_step/configs/crafter_synth_backend.md +0 -40
  350. examples/multi_step/configs/verilog_eval_groq_qwen32b.toml +0 -31
  351. examples/multi_step/configs/verilog_eval_synth_qwen8b.toml +0 -33
  352. examples/multi_step/configs/verilog_rl_lora.toml +0 -190
  353. examples/multi_step/crafter_rl_lora.md +0 -70
  354. examples/multi_step/judges/crafter_backend_judge.py +0 -220
  355. examples/multi_step/judges/verilog_backend_judge.py +0 -234
  356. examples/multi_step/readme.md +0 -48
  357. examples/multi_step/sse_metrics_streaming_notes.md +0 -357
  358. examples/multi_step/task_app_config_notes.md +0 -494
  359. examples/multi_step/verilog_rl_lora.md +0 -218
  360. examples/qwen_coder/README.md +0 -102
  361. examples/qwen_coder/_shared.py +0 -113
  362. examples/qwen_coder/configs/coder_lora_30b.toml +0 -61
  363. examples/qwen_coder/configs/coder_lora_4b.toml +0 -57
  364. examples/qwen_coder/configs/coder_lora_small.toml +0 -58
  365. examples/qwen_coder/generate_dataset.py +0 -98
  366. examples/qwen_coder/infer_ft_smoke.py +0 -65
  367. examples/qwen_coder/infer_prod_proxy.py +0 -73
  368. examples/qwen_coder/infer_via_synth.py +0 -87
  369. examples/qwen_coder/scripts/infer_coder.sh +0 -19
  370. examples/qwen_coder/scripts/train_coder_30b.sh +0 -22
  371. examples/qwen_coder/sft_full_17b.py +0 -103
  372. examples/qwen_coder/sft_lora_30b.py +0 -110
  373. examples/qwen_coder/subset_jsonl.py +0 -39
  374. examples/qwen_coder/todos.md +0 -38
  375. examples/qwen_coder/validate_jsonl.py +0 -60
  376. examples/rl/README.md +0 -169
  377. examples/rl/download_dataset.py +0 -80
  378. examples/run_crafter_demo.sh +0 -10
  379. examples/sft/README.md +0 -139
  380. examples/sft/configs/crafter_fft_qwen0p6b.toml +0 -44
  381. examples/sft/configs/crafter_lora_qwen0p6b.toml +0 -45
  382. examples/sft/evaluate.py +0 -119
  383. examples/sft/export_dataset.py +0 -117
  384. examples/sft/generate_traces.py +0 -164
  385. examples/swe/__init__.py +0 -12
  386. examples/swe/task_app/README.md +0 -105
  387. examples/swe/task_app/__init__.py +0 -2
  388. examples/swe/task_app/grpo_swe_mini.py +0 -601
  389. examples/swe/task_app/grpo_swe_mini_task_app.py +0 -136
  390. examples/swe/task_app/hosted/README.md +0 -173
  391. examples/swe/task_app/hosted/__init__.py +0 -5
  392. examples/swe/task_app/hosted/branching.py +0 -143
  393. examples/swe/task_app/hosted/environment_routes.py +0 -1289
  394. examples/swe/task_app/hosted/envs/__init__.py +0 -1
  395. examples/swe/task_app/hosted/envs/crafter/__init__.py +0 -6
  396. examples/swe/task_app/hosted/envs/crafter/app.py +0 -1
  397. examples/swe/task_app/hosted/envs/crafter/environment.py +0 -522
  398. examples/swe/task_app/hosted/envs/crafter/policy.py +0 -478
  399. examples/swe/task_app/hosted/envs/crafter/react_agent.py +0 -108
  400. examples/swe/task_app/hosted/envs/crafter/shared.py +0 -305
  401. examples/swe/task_app/hosted/envs/crafter/tools.py +0 -47
  402. examples/swe/task_app/hosted/envs/mini_swe/__init__.py +0 -8
  403. examples/swe/task_app/hosted/envs/mini_swe/environment.py +0 -1164
  404. examples/swe/task_app/hosted/envs/mini_swe/policy.py +0 -355
  405. examples/swe/task_app/hosted/envs/mini_swe/shared.py +0 -83
  406. examples/swe/task_app/hosted/envs/mini_swe/tools.py +0 -96
  407. examples/swe/task_app/hosted/hosted_app.py +0 -204
  408. examples/swe/task_app/hosted/inference/__init__.py +0 -5
  409. examples/swe/task_app/hosted/inference/openai_client.py +0 -618
  410. examples/swe/task_app/hosted/main.py +0 -100
  411. examples/swe/task_app/hosted/policy_routes.py +0 -1079
  412. examples/swe/task_app/hosted/registry.py +0 -195
  413. examples/swe/task_app/hosted/rollout.py +0 -1911
  414. examples/swe/task_app/hosted/storage/__init__.py +0 -5
  415. examples/swe/task_app/hosted/storage/volume.py +0 -211
  416. examples/swe/task_app/hosted/test_agents.py +0 -161
  417. examples/swe/task_app/hosted/test_service.py +0 -136
  418. examples/swe/task_app/hosted/utils.py +0 -62
  419. examples/task_apps/IMAGE_ONLY_EVAL_QUICKSTART.md +0 -258
  420. examples/task_apps/TESTING.md +0 -275
  421. examples/task_apps/crafter/CREATE_SFT_DATASET.md +0 -273
  422. examples/task_apps/crafter/EVAL_IMAGE_ONLY_RESULTS.md +0 -152
  423. examples/task_apps/crafter/FILTER_COMMAND_STATUS.md +0 -174
  424. examples/task_apps/crafter/FILTER_COMMAND_SUCCESS.md +0 -268
  425. examples/task_apps/crafter/QUERY_EXAMPLES.md +0 -203
  426. examples/task_apps/crafter/README_IMAGE_ONLY_EVAL.md +0 -316
  427. examples/task_apps/crafter/__init__.py +0 -0
  428. examples/task_apps/crafter/eval_image_only_gpt4o.toml +0 -28
  429. examples/task_apps/crafter/eval_text_only_groq_llama.toml +0 -36
  430. examples/task_apps/crafter/filter_sft_dataset.toml +0 -16
  431. examples/task_apps/crafter/task_app/README.md +0 -42
  432. examples/task_apps/crafter/task_app/__init__.py +0 -5
  433. examples/task_apps/crafter/task_app/grpo_crafter.py +0 -973
  434. examples/task_apps/crafter/task_app/grpo_crafter_task_app.py +0 -146
  435. examples/task_apps/crafter/task_app/synth_envs_hosted/README.md +0 -173
  436. examples/task_apps/crafter/task_app/synth_envs_hosted/__init__.py +0 -5
  437. examples/task_apps/crafter/task_app/synth_envs_hosted/branching.py +0 -143
  438. examples/task_apps/crafter/task_app/synth_envs_hosted/environment_routes.py +0 -1226
  439. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/__init__.py +0 -1
  440. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/__init__.py +0 -6
  441. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/app.py +0 -1
  442. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/environment.py +0 -532
  443. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +0 -547
  444. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/react_agent.py +0 -123
  445. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/shared.py +0 -305
  446. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/tools.py +0 -47
  447. examples/task_apps/crafter/task_app/synth_envs_hosted/hosted_app.py +0 -204
  448. examples/task_apps/crafter/task_app/synth_envs_hosted/inference/__init__.py +0 -5
  449. examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +0 -704
  450. examples/task_apps/crafter/task_app/synth_envs_hosted/main.py +0 -100
  451. examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +0 -1152
  452. examples/task_apps/crafter/task_app/synth_envs_hosted/registry.py +0 -195
  453. examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +0 -2160
  454. examples/task_apps/crafter/task_app/synth_envs_hosted/storage/__init__.py +0 -5
  455. examples/task_apps/crafter/task_app/synth_envs_hosted/storage/volume.py +0 -211
  456. examples/task_apps/crafter/task_app/synth_envs_hosted/test_agents.py +0 -161
  457. examples/task_apps/crafter/task_app/synth_envs_hosted/test_service.py +0 -136
  458. examples/task_apps/crafter/task_app/synth_envs_hosted/utils.py +0 -218
  459. examples/task_apps/dev/pokemon_emerald/__init__.py +0 -2
  460. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/README.md +0 -811
  461. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/__init__.py +0 -120
  462. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/action.py +0 -160
  463. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/memory.py +0 -155
  464. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/perception.py +0 -69
  465. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/planning.py +0 -96
  466. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/simple.py +0 -1502
  467. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/system_prompt.py +0 -4
  468. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/grab_map.py +0 -68
  469. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/manual.py +0 -216
  470. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/__init__.py +0 -35
  471. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/emerald_utils.py +0 -631
  472. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/emulator.py +0 -1544
  473. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/enums.py +0 -1428
  474. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/memory_reader.py +0 -4848
  475. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/types.py +0 -41
  476. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/utils.py +0 -298
  477. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pyproject.toml +0 -95
  478. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/run.py +0 -204
  479. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/__init__.py +0 -0
  480. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/app.py +0 -2152
  481. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/client.py +0 -429
  482. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/frame_server.py +0 -155
  483. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/README.md +0 -78
  484. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/__init__.py +0 -0
  485. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/run_tests.py +0 -122
  486. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_agent_direct.py +0 -76
  487. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_agent_prompts.py +0 -413
  488. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_battle_state_formatting.py +0 -204
  489. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_dialogue_detection.py +0 -133
  490. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_dialogue_detection_comprehensive.py +0 -229
  491. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_direct_agent_emulator.py +0 -300
  492. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_fps_adjustment_pytest.py +0 -205
  493. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_house_to_outside_direct.py +0 -200
  494. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_house_to_outside_transition.py +0 -284
  495. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_map_ground_truth_comparison.py +0 -468
  496. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_memory_map.py +0 -575
  497. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_server_map_validation.py +0 -311
  498. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_torchic_state.py +0 -259
  499. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/__init__.py +0 -0
  500. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/anticheat.py +0 -372
  501. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/checkpoint.py +0 -296
  502. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/error_handler.py +0 -275
  503. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/get_local_ip.py +0 -22
  504. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/helpers.py +0 -44
  505. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/llm_logger.py +0 -514
  506. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_formatter.py +0 -415
  507. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_stitcher.py +0 -1763
  508. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_stitcher_singleton.py +0 -33
  509. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_trimmer.py +0 -106
  510. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_visualizer.py +0 -334
  511. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/ocr_dialogue.py +0 -1020
  512. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/recording.py +0 -188
  513. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/state_formatter.py +0 -1481
  514. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/vlm.py +0 -862
  515. examples/task_apps/dev/pokemon_emerald/modal_app.py +0 -114
  516. examples/task_apps/dev/pokemon_emerald/task_app/README.md +0 -81
  517. examples/task_apps/dev/pokemon_emerald/task_app/__init__.py +0 -6
  518. examples/task_apps/dev/pokemon_emerald/task_app/pokemon_emerald.py +0 -685
  519. examples/task_apps/enron/__init__.py +0 -1
  520. examples/task_apps/enron/eval_groq_qwen32.toml +0 -16
  521. examples/task_apps/enron/filter_sft.toml +0 -5
  522. examples/task_apps/enron/task_app/README.md +0 -14
  523. examples/task_apps/enron/task_app/__init__.py +0 -1
  524. examples/task_apps/enron/task_app/grpo_enron.py +0 -906
  525. examples/task_apps/enron/task_app/grpo_enron_task_app.py +0 -146
  526. examples/task_apps/enron/tests/__init__.py +0 -4
  527. examples/task_apps/enron/tests/conftest.py +0 -115
  528. examples/task_apps/enron/tests/integration/__init__.py +0 -4
  529. examples/task_apps/enron/tests/integration/test_enron_eval.py +0 -179
  530. examples/task_apps/enron/tests/integration/test_enron_rollout.py +0 -135
  531. examples/task_apps/enron/tests/unit/__init__.py +0 -4
  532. examples/task_apps/enron/tests/unit/test_enron_environment.py +0 -126
  533. examples/task_apps/math/README.md +0 -22
  534. examples/task_apps/math/__init__.py +0 -0
  535. examples/task_apps/math/math_single_step.py +0 -1000
  536. examples/task_apps/math/math_task_app.py +0 -115
  537. examples/task_apps/pokemon_battle/__init__.py +0 -2
  538. examples/task_apps/pokemon_battle/modal_app.py +0 -104
  539. examples/task_apps/pokemon_battle/task_app/README.md +0 -68
  540. examples/task_apps/pokemon_battle/task_app/__init__.py +0 -6
  541. examples/task_apps/pokemon_battle/task_app/pokemon_showdown.py +0 -932
  542. examples/task_apps/pokemon_red/EVAL_IMAGE_ONLY_COMPLETE.md +0 -283
  543. examples/task_apps/pokemon_red/EVAL_IMAGE_ONLY_STATUS.md +0 -155
  544. examples/task_apps/pokemon_red/README.md +0 -357
  545. examples/task_apps/pokemon_red/README_IMAGE_ONLY_EVAL.md +0 -415
  546. examples/task_apps/pokemon_red/__init__.py +0 -3
  547. examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +0 -29
  548. examples/task_apps/pokemon_red/eval_pokemon_red_policy.py +0 -225
  549. examples/task_apps/pokemon_red/pallet_town_rl_config.toml +0 -75
  550. examples/task_apps/pokemon_red/task_app.py +0 -799
  551. examples/task_apps/pokemon_red/test_pallet_town_rewards.py +0 -193
  552. examples/task_apps/sokoban/README.md +0 -307
  553. examples/task_apps/sokoban/__init__.py +0 -3
  554. examples/task_apps/sokoban/eval_groq_qwen32.toml +0 -16
  555. examples/task_apps/sokoban/eval_openai_gpt5.toml +0 -16
  556. examples/task_apps/sokoban/filter_sft.toml +0 -5
  557. examples/task_apps/sokoban/task_app.py +0 -1058
  558. examples/task_apps/sokoban/tests/__init__.py +0 -4
  559. examples/task_apps/sokoban/tests/conftest.py +0 -113
  560. examples/task_apps/sokoban/tests/integration/__init__.py +0 -4
  561. examples/task_apps/sokoban/tests/integration/test_sokoban_eval.py +0 -57
  562. examples/task_apps/sokoban/tests/integration/test_sokoban_rollout.py +0 -198
  563. examples/task_apps/sokoban/tests/unit/__init__.py +0 -4
  564. examples/task_apps/sokoban/tests/unit/test_sokoban_environment.py +0 -114
  565. examples/task_apps/verilog/__init__.py +0 -1
  566. examples/task_apps/verilog/eval_groq_qwen32b.toml +0 -24
  567. examples/task_apps/verilog/filter_sft.toml +0 -5
  568. examples/task_apps/verilog/task_app/README.md +0 -12
  569. examples/task_apps/verilog/task_app/__init__.py +0 -1
  570. examples/task_apps/verilog/task_app/grpo_verilog.py +0 -1166
  571. examples/task_apps/verilog/task_app/grpo_verilog_task_app.py +0 -145
  572. examples/task_apps/verilog/tests/__init__.py +0 -4
  573. examples/task_apps/verilog/tests/conftest.py +0 -115
  574. examples/task_apps/verilog/tests/integration/__init__.py +0 -4
  575. examples/task_apps/verilog/tests/integration/test_verilog_eval.py +0 -181
  576. examples/task_apps/verilog/tests/integration/test_verilog_rollout.py +0 -55
  577. examples/task_apps/verilog/tests/unit/__init__.py +0 -4
  578. examples/task_apps/verilog/tests/unit/test_verilog_scoring.py +0 -118
  579. examples/vlm/PROPOSAL.md +0 -53
  580. examples/vlm/README.md +0 -68
  581. examples/vlm/configs/crafter_vlm_gpt4o.toml +0 -44
  582. examples/vlm/crafter_image_only_agent.py +0 -207
  583. examples/vlm/crafter_openai_vlm_agent.py +0 -277
  584. examples/vlm/filter_image_rows.py +0 -63
  585. examples/vlm/run_crafter_vlm_benchmark.py +0 -316
  586. examples/warming_up_to_rl/analyze_trace_db.py +0 -422
  587. examples/warming_up_to_rl/configs/crafter_fft.toml +0 -48
  588. examples/warming_up_to_rl/configs/crafter_fft_4b.toml +0 -54
  589. examples/warming_up_to_rl/configs/eval_fft_qwen4b.toml +0 -20
  590. examples/warming_up_to_rl/configs/eval_groq_qwen32b.toml +0 -13
  591. examples/warming_up_to_rl/configs/eval_modal_qwen4b.toml +0 -23
  592. examples/warming_up_to_rl/configs/eval_stepwise_complex.toml +0 -35
  593. examples/warming_up_to_rl/configs/eval_stepwise_consistent.toml +0 -26
  594. examples/warming_up_to_rl/configs/eval_stepwise_per_achievement.toml +0 -36
  595. examples/warming_up_to_rl/configs/eval_stepwise_simple.toml +0 -32
  596. examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +0 -83
  597. examples/warming_up_to_rl/configs/rl_from_ft.toml +0 -56
  598. examples/warming_up_to_rl/export_trace_sft.py +0 -723
  599. examples/warming_up_to_rl/groq_test.py +0 -97
  600. examples/warming_up_to_rl/manage_secrets.py +0 -131
  601. examples/warming_up_to_rl/old/event_rewards.md +0 -234
  602. examples/warming_up_to_rl/old/notes.md +0 -73
  603. examples/warming_up_to_rl/readme.md +0 -179
  604. examples/warming_up_to_rl/run_eval.py +0 -736
  605. examples/warming_up_to_rl/run_fft_and_save.py +0 -380
  606. examples/warming_up_to_rl/run_local_rollout.py +0 -239
  607. examples/warming_up_to_rl/run_local_rollout_modal.py +0 -248
  608. examples/warming_up_to_rl/run_local_rollout_parallel.py +0 -405
  609. examples/warming_up_to_rl/run_local_rollout_traced.py +0 -477
  610. examples/warming_up_to_rl/run_rl_and_save.py +0 -124
  611. examples/warming_up_to_rl/run_rollout_remote.py +0 -156
  612. examples/workflows/__init__.py +0 -0
  613. examples/workflows/math_rl/__init__.py +0 -0
  614. examples/workflows/math_rl/configs/eval_base_qwen.toml +0 -15
  615. examples/workflows/math_rl/configs/eval_rl_qwen.toml +0 -11
  616. examples/workflows/math_rl/configs/rl_from_base_qwen.toml +0 -35
  617. examples/workflows/math_rl/configs/rl_from_base_qwen17.toml +0 -74
  618. examples/workflows/math_rl/configs/rl_from_ft_qwen.toml +0 -35
  619. examples/workflows/math_rl/download_dataset.py +0 -80
  620. examples/workflows/math_rl/run_eval.py +0 -436
  621. examples/workflows/math_rl/run_rl_and_save.py +0 -111
  622. synth_ai/api/models/supported.py +0 -377
  623. synth_ai/api/train/__init__.py +0 -5
  624. synth_ai/api/train/builders.py +0 -351
  625. synth_ai/api/train/cli.py +0 -635
  626. synth_ai/api/train/config_finder.py +0 -228
  627. synth_ai/api/train/configs/__init__.py +0 -44
  628. synth_ai/api/train/configs/rl.py +0 -134
  629. synth_ai/api/train/configs/sft.py +0 -95
  630. synth_ai/api/train/configs/shared.py +0 -24
  631. synth_ai/api/train/env_resolver.py +0 -349
  632. synth_ai/api/train/pollers.py +0 -75
  633. synth_ai/api/train/supported_algos.py +0 -147
  634. synth_ai/api/train/task_app.py +0 -195
  635. synth_ai/api/train/utils.py +0 -225
  636. synth_ai/cli/_modal_wrapper.py +0 -29
  637. synth_ai/cli/_storage.py +0 -20
  638. synth_ai/cli/_typer_patch.py +0 -49
  639. synth_ai/cli/_validate_task_app.py +0 -11
  640. synth_ai/cli/balance.py +0 -216
  641. synth_ai/cli/calc.py +0 -84
  642. synth_ai/cli/demo.py +0 -165
  643. synth_ai/cli/legacy_root_backup.py +0 -468
  644. synth_ai/cli/man.py +0 -106
  645. synth_ai/cli/recent.py +0 -132
  646. synth_ai/cli/rl_demo.py +0 -254
  647. synth_ai/cli/status.py +0 -134
  648. synth_ai/cli/task_apps.py +0 -4523
  649. synth_ai/cli/traces.py +0 -164
  650. synth_ai/cli/tui.py +0 -57
  651. synth_ai/cli/watch.py +0 -506
  652. synth_ai/compound/cais.py +0 -0
  653. synth_ai/config/base_url.py +0 -107
  654. synth_ai/core/experiment.py +0 -13
  655. synth_ai/core/system.py +0 -15
  656. synth_ai/demo_registry.py +0 -295
  657. synth_ai/demos/core/__init__.py +0 -1
  658. synth_ai/demos/core/cli.py +0 -1718
  659. synth_ai/demos/demo_task_apps/core.py +0 -440
  660. synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +0 -184
  661. synth_ai/demos/demo_task_apps/math/deploy_task_app.sh +0 -22
  662. synth_ai/demos/demo_task_apps/math/modal_task_app.py +0 -739
  663. synth_ai/demos/demo_task_apps/math/task_app_entry.py +0 -37
  664. synth_ai/environments/__init__.py +0 -31
  665. synth_ai/environments/environment/__init__.py +0 -1
  666. synth_ai/environments/environment/artifacts/__init__.py +0 -1
  667. synth_ai/environments/environment/artifacts/base.py +0 -52
  668. synth_ai/environments/environment/core.py +0 -67
  669. synth_ai/environments/environment/db/__init__.py +0 -1
  670. synth_ai/environments/environment/db/sqlite.py +0 -45
  671. synth_ai/environments/environment/registry.py +0 -233
  672. synth_ai/environments/environment/resources/sqlite.py +0 -45
  673. synth_ai/environments/environment/results.py +0 -1
  674. synth_ai/environments/environment/rewards/__init__.py +0 -1
  675. synth_ai/environments/environment/rewards/core.py +0 -29
  676. synth_ai/environments/environment/shared_engine.py +0 -26
  677. synth_ai/environments/environment/tools/__init__.py +0 -200
  678. synth_ai/environments/examples/__init__.py +0 -1
  679. synth_ai/environments/examples/bandit/__init__.py +0 -33
  680. synth_ai/environments/examples/bandit/engine.py +0 -302
  681. synth_ai/environments/examples/bandit/environment.py +0 -194
  682. synth_ai/environments/examples/bandit/taskset.py +0 -200
  683. synth_ai/environments/examples/crafter_classic/__init__.py +0 -8
  684. synth_ai/environments/examples/crafter_classic/agent_demos/analyze_semantic_words_markdown.py +0 -250
  685. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_comprehensive_evaluation.py +0 -59
  686. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_evaluation_browser.py +0 -152
  687. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_evaluation_config.toml +0 -24
  688. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_evaluation_framework.py +0 -1194
  689. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/crafter_synth_config.toml +0 -56
  690. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/filter_config_modal.toml +0 -32
  691. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/filter_traces_sft_turso.py +0 -738
  692. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/kick_off_ft_modal.py +0 -384
  693. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_action_results.py +0 -53
  694. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_agent_actions.py +0 -178
  695. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_latest_run.py +0 -222
  696. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_lm_traces.py +0 -183
  697. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_no_rewards.py +0 -210
  698. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_trace_issue.py +0 -206
  699. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/check_db_schema.py +0 -49
  700. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/check_latest_results.py +0 -64
  701. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/debug_agent_responses.py +0 -88
  702. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/quick_trace_check.py +0 -77
  703. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/compare_experiments.py +0 -324
  704. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/filter_traces_sft_turso.py +0 -580
  705. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/kick_off_ft_oai.py +0 -362
  706. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/multi_model_config.toml +0 -49
  707. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/analyze_enhanced_hooks.py +0 -332
  708. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/analyze_hook_events.py +0 -97
  709. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/analyze_hook_results.py +0 -217
  710. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/check_hook_storage.py +0 -87
  711. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/check_seeds.py +0 -88
  712. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/compare_seed_performance.py +0 -195
  713. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/custom_eval_pipelines.py +0 -400
  714. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/plot_hook_frequency.py +0 -195
  715. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/seed_analysis_summary.py +0 -56
  716. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/run_rollouts_for_models_and_compare_v3.py +0 -858
  717. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_quick_evaluation.py +0 -52
  718. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_react_agent.py +0 -874
  719. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_trace_evaluation.py +0 -1412
  720. synth_ai/environments/examples/crafter_classic/agent_demos/example_v3_usage.py +0 -216
  721. synth_ai/environments/examples/crafter_classic/agent_demos/old/compare_traces.py +0 -296
  722. synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_comprehensive_evaluation.py +0 -58
  723. synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_env_serialization.py +0 -464
  724. synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_evaluation_browser.py +0 -152
  725. synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_quick_evaluation.py +0 -51
  726. synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_trace_evaluation.py +0 -1412
  727. synth_ai/environments/examples/crafter_classic/agent_demos/old/debug_player_loss.py +0 -112
  728. synth_ai/environments/examples/crafter_classic/agent_demos/old/diagnose_service.py +0 -203
  729. synth_ai/environments/examples/crafter_classic/agent_demos/old/diagnose_slowness.py +0 -305
  730. synth_ai/environments/examples/crafter_classic/agent_demos/old/eval_by_difficulty.py +0 -126
  731. synth_ai/environments/examples/crafter_classic/agent_demos/old/eval_example.py +0 -94
  732. synth_ai/environments/examples/crafter_classic/agent_demos/old/explore_saved_states.py +0 -142
  733. synth_ai/environments/examples/crafter_classic/agent_demos/old/filter_traces_sft.py +0 -26
  734. synth_ai/environments/examples/crafter_classic/agent_demos/old/filter_traces_sft_OLD.py +0 -984
  735. synth_ai/environments/examples/crafter_classic/agent_demos/old/generate_ft_data_gemini.py +0 -724
  736. synth_ai/environments/examples/crafter_classic/agent_demos/old/generate_ft_data_modal.py +0 -386
  737. synth_ai/environments/examples/crafter_classic/agent_demos/old/generate_ft_metadata.py +0 -205
  738. synth_ai/environments/examples/crafter_classic/agent_demos/old/kick_off_ft_gemini.py +0 -150
  739. synth_ai/environments/examples/crafter_classic/agent_demos/old/kick_off_ft_modal.py +0 -283
  740. synth_ai/environments/examples/crafter_classic/agent_demos/old/prepare_vertex_ft.py +0 -280
  741. synth_ai/environments/examples/crafter_classic/agent_demos/old/profile_env_slowness.py +0 -456
  742. synth_ai/environments/examples/crafter_classic/agent_demos/old/replicate_issue.py +0 -166
  743. synth_ai/environments/examples/crafter_classic/agent_demos/old/run_and_eval.py +0 -102
  744. synth_ai/environments/examples/crafter_classic/agent_demos/old/run_comparison.py +0 -128
  745. synth_ai/environments/examples/crafter_classic/agent_demos/old/run_qwen_rollouts.py +0 -655
  746. synth_ai/environments/examples/crafter_classic/agent_demos/old/trace_eval_OLD.py +0 -202
  747. synth_ai/environments/examples/crafter_classic/agent_demos/old/validate_openai_format.py +0 -166
  748. synth_ai/environments/examples/crafter_classic/config_logging.py +0 -111
  749. synth_ai/environments/examples/crafter_classic/debug_translation.py +0 -0
  750. synth_ai/environments/examples/crafter_classic/engine.py +0 -579
  751. synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +0 -64
  752. synth_ai/environments/examples/crafter_classic/engine_helpers/action_map.py +0 -6
  753. synth_ai/environments/examples/crafter_classic/engine_helpers/serialization.py +0 -75
  754. synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +0 -267
  755. synth_ai/environments/examples/crafter_classic/environment.py +0 -495
  756. synth_ai/environments/examples/crafter_classic/taskset.py +0 -233
  757. synth_ai/environments/examples/crafter_classic/trace_hooks_v3.py +0 -228
  758. synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +0 -299
  759. synth_ai/environments/examples/crafter_custom/__init__.py +0 -4
  760. synth_ai/environments/examples/crafter_custom/agent_demos/__init__.py +0 -1
  761. synth_ai/environments/examples/crafter_custom/agent_demos/trace_eval.py +0 -202
  762. synth_ai/environments/examples/crafter_custom/crafter/__init__.py +0 -7
  763. synth_ai/environments/examples/crafter_custom/crafter/config.py +0 -182
  764. synth_ai/environments/examples/crafter_custom/crafter/constants.py +0 -8
  765. synth_ai/environments/examples/crafter_custom/crafter/engine.py +0 -269
  766. synth_ai/environments/examples/crafter_custom/crafter/env.py +0 -262
  767. synth_ai/environments/examples/crafter_custom/crafter/objects.py +0 -417
  768. synth_ai/environments/examples/crafter_custom/crafter/recorder.py +0 -187
  769. synth_ai/environments/examples/crafter_custom/crafter/worldgen.py +0 -118
  770. synth_ai/environments/examples/crafter_custom/dataset_builder.py +0 -373
  771. synth_ai/environments/examples/crafter_custom/environment.py +0 -312
  772. synth_ai/environments/examples/crafter_custom/old/analyze_diamond_issue.py +0 -159
  773. synth_ai/environments/examples/crafter_custom/old/analyze_diamond_spawning.py +0 -158
  774. synth_ai/environments/examples/crafter_custom/old/compare_worlds.py +0 -71
  775. synth_ai/environments/examples/crafter_custom/old/dataset_stats.py +0 -105
  776. synth_ai/environments/examples/crafter_custom/old/diamond_spawning_summary.py +0 -119
  777. synth_ai/environments/examples/crafter_custom/old/example_dataset_usage.py +0 -52
  778. synth_ai/environments/examples/crafter_custom/run_dataset.py +0 -305
  779. synth_ai/environments/examples/enron/art_helpers/email_search_tools.py +0 -156
  780. synth_ai/environments/examples/enron/art_helpers/local_email_db.py +0 -281
  781. synth_ai/environments/examples/enron/art_helpers/types_enron.py +0 -25
  782. synth_ai/environments/examples/enron/engine.py +0 -300
  783. synth_ai/environments/examples/enron/environment.py +0 -234
  784. synth_ai/environments/examples/enron/taskset.py +0 -112
  785. synth_ai/environments/examples/enron/units/keyword_stats.py +0 -112
  786. synth_ai/environments/examples/minigrid/__init__.py +0 -48
  787. synth_ai/environments/examples/minigrid/agent_demos/minigrid_evaluation_framework.py +0 -1188
  788. synth_ai/environments/examples/minigrid/agent_demos/minigrid_quick_evaluation.py +0 -48
  789. synth_ai/environments/examples/minigrid/agent_demos/minigrid_react_agent.py +0 -562
  790. synth_ai/environments/examples/minigrid/agent_demos/minigrid_trace_evaluation.py +0 -221
  791. synth_ai/environments/examples/minigrid/engine.py +0 -589
  792. synth_ai/environments/examples/minigrid/environment.py +0 -274
  793. synth_ai/environments/examples/minigrid/environment_mapping.py +0 -242
  794. synth_ai/environments/examples/minigrid/puzzle_loader.py +0 -417
  795. synth_ai/environments/examples/minigrid/taskset.py +0 -583
  796. synth_ai/environments/examples/nethack/__init__.py +0 -7
  797. synth_ai/environments/examples/nethack/achievements.py +0 -337
  798. synth_ai/environments/examples/nethack/agent_demos/nethack_evaluation_framework.py +0 -981
  799. synth_ai/environments/examples/nethack/agent_demos/nethack_quick_evaluation.py +0 -74
  800. synth_ai/environments/examples/nethack/agent_demos/nethack_react_agent.py +0 -831
  801. synth_ai/environments/examples/nethack/engine.py +0 -739
  802. synth_ai/environments/examples/nethack/environment.py +0 -256
  803. synth_ai/environments/examples/nethack/helpers/__init__.py +0 -41
  804. synth_ai/environments/examples/nethack/helpers/action_mapping.py +0 -301
  805. synth_ai/environments/examples/nethack/helpers/nle_wrapper.py +0 -402
  806. synth_ai/environments/examples/nethack/helpers/observation_utils.py +0 -433
  807. synth_ai/environments/examples/nethack/helpers/recording_wrapper.py +0 -200
  808. synth_ai/environments/examples/nethack/helpers/trajectory_recorder.py +0 -269
  809. synth_ai/environments/examples/nethack/helpers/visualization/replay_viewer.py +0 -308
  810. synth_ai/environments/examples/nethack/helpers/visualization/visualizer.py +0 -431
  811. synth_ai/environments/examples/nethack/taskset.py +0 -323
  812. synth_ai/environments/examples/red/__init__.py +0 -7
  813. synth_ai/environments/examples/red/agent_demos/__init__.py +0 -1
  814. synth_ai/environments/examples/red/config_logging.py +0 -110
  815. synth_ai/environments/examples/red/engine.py +0 -721
  816. synth_ai/environments/examples/red/engine_helpers/__init__.py +0 -1
  817. synth_ai/environments/examples/red/engine_helpers/memory_map.py +0 -35
  818. synth_ai/environments/examples/red/engine_helpers/reward_components.py +0 -276
  819. synth_ai/environments/examples/red/engine_helpers/reward_library/__init__.py +0 -142
  820. synth_ai/environments/examples/red/engine_helpers/reward_library/adaptive_rewards.py +0 -57
  821. synth_ai/environments/examples/red/engine_helpers/reward_library/battle_rewards.py +0 -284
  822. synth_ai/environments/examples/red/engine_helpers/reward_library/composite_rewards.py +0 -150
  823. synth_ai/environments/examples/red/engine_helpers/reward_library/economy_rewards.py +0 -138
  824. synth_ai/environments/examples/red/engine_helpers/reward_library/efficiency_rewards.py +0 -57
  825. synth_ai/environments/examples/red/engine_helpers/reward_library/exploration_rewards.py +0 -331
  826. synth_ai/environments/examples/red/engine_helpers/reward_library/novelty_rewards.py +0 -121
  827. synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_progression.py +0 -477
  828. synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_rewards.py +0 -559
  829. synth_ai/environments/examples/red/engine_helpers/reward_library/pokemon_rewards.py +0 -313
  830. synth_ai/environments/examples/red/engine_helpers/reward_library/social_rewards.py +0 -148
  831. synth_ai/environments/examples/red/engine_helpers/reward_library/story_rewards.py +0 -247
  832. synth_ai/environments/examples/red/engine_helpers/screen_analysis.py +0 -368
  833. synth_ai/environments/examples/red/engine_helpers/state_extraction.py +0 -172
  834. synth_ai/environments/examples/red/environment.py +0 -298
  835. synth_ai/environments/examples/red/taskset.py +0 -79
  836. synth_ai/environments/examples/red/units/__init__.py +0 -1
  837. synth_ai/environments/examples/sokoban/__init__.py +0 -1
  838. synth_ai/environments/examples/sokoban/agent_demos/sokoban_full_eval.py +0 -899
  839. synth_ai/environments/examples/sokoban/engine.py +0 -678
  840. synth_ai/environments/examples/sokoban/engine_helpers/__init__.py +0 -1
  841. synth_ai/environments/examples/sokoban/engine_helpers/room_utils.py +0 -657
  842. synth_ai/environments/examples/sokoban/engine_helpers/vendored/__init__.py +0 -18
  843. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/__init__.py +0 -3
  844. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/boxoban_env.py +0 -131
  845. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/render_utils.py +0 -370
  846. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/room_utils.py +0 -332
  847. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env.py +0 -306
  848. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_fixed_targets.py +0 -67
  849. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_pull.py +0 -115
  850. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_two_player.py +0 -123
  851. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_variations.py +0 -394
  852. synth_ai/environments/examples/sokoban/environment.py +0 -229
  853. synth_ai/environments/examples/sokoban/generate_verified_puzzles.py +0 -440
  854. synth_ai/environments/examples/sokoban/puzzle_loader.py +0 -312
  855. synth_ai/environments/examples/sokoban/taskset.py +0 -544
  856. synth_ai/environments/examples/tictactoe/__init__.py +0 -1
  857. synth_ai/environments/examples/tictactoe/engine.py +0 -368
  858. synth_ai/environments/examples/tictactoe/environment.py +0 -240
  859. synth_ai/environments/examples/tictactoe/taskset.py +0 -215
  860. synth_ai/environments/examples/verilog/__init__.py +0 -10
  861. synth_ai/environments/examples/verilog/engine.py +0 -421
  862. synth_ai/environments/examples/verilog/environment.py +0 -350
  863. synth_ai/environments/examples/verilog/taskset.py +0 -420
  864. synth_ai/environments/examples/wordle/__init__.py +0 -29
  865. synth_ai/environments/examples/wordle/engine.py +0 -398
  866. synth_ai/environments/examples/wordle/environment.py +0 -159
  867. synth_ai/environments/examples/wordle/helpers/generate_instances_wordfreq.py +0 -75
  868. synth_ai/environments/examples/wordle/taskset.py +0 -230
  869. synth_ai/environments/reproducibility/core.py +0 -42
  870. synth_ai/environments/reproducibility/helpers.py +0 -0
  871. synth_ai/environments/reproducibility/tree.py +0 -363
  872. synth_ai/environments/service/app.py +0 -97
  873. synth_ai/environments/service/core_routes.py +0 -1021
  874. synth_ai/environments/service/external_registry.py +0 -56
  875. synth_ai/environments/service/registry.py +0 -9
  876. synth_ai/environments/stateful/__init__.py +0 -1
  877. synth_ai/environments/stateful/core.py +0 -163
  878. synth_ai/environments/stateful/engine.py +0 -21
  879. synth_ai/environments/stateful/state.py +0 -7
  880. synth_ai/environments/tasks/api.py +0 -19
  881. synth_ai/environments/tasks/core.py +0 -81
  882. synth_ai/environments/tasks/filters.py +0 -40
  883. synth_ai/environments/tasks/utils.py +0 -90
  884. synth_ai/environments/v0_observability/history.py +0 -3
  885. synth_ai/environments/v0_observability/log.py +0 -2
  886. synth_ai/evals/__init__.py +0 -15
  887. synth_ai/evals/base.py +0 -13
  888. synth_ai/evals/client.py +0 -82
  889. synth_ai/handshake.py +0 -109
  890. synth_ai/http.py +0 -26
  891. synth_ai/http_client.py +0 -136
  892. synth_ai/inference/__init__.py +0 -5
  893. synth_ai/inference/client.py +0 -34
  894. synth_ai/jobs/client.py +0 -295
  895. synth_ai/judge_schemas.py +0 -127
  896. synth_ai/learning/__init__.py +0 -59
  897. synth_ai/learning/client.py +0 -241
  898. synth_ai/learning/ft_client.py +0 -7
  899. synth_ai/learning/health.py +0 -49
  900. synth_ai/learning/jobs.py +0 -201
  901. synth_ai/learning/rl/client.py +0 -267
  902. synth_ai/learning/rl/contracts.py +0 -27
  903. synth_ai/learning/rl/env_keys.py +0 -166
  904. synth_ai/learning/rl/secrets.py +0 -13
  905. synth_ai/learning/sft/client.py +0 -68
  906. synth_ai/learning/sft/config.py +0 -270
  907. synth_ai/learning/sft/data.py +0 -295
  908. synth_ai/learning/validators.py +0 -49
  909. synth_ai/lm/__init__.py +0 -25
  910. synth_ai/task/__init__.py +0 -121
  911. synth_ai/task/apps/__init__.py +0 -129
  912. synth_ai/task/config.py +0 -257
  913. synth_ai/task/contracts.py +0 -236
  914. synth_ai/task/datasets.py +0 -108
  915. synth_ai/task/proxy.py +0 -251
  916. synth_ai/task/rubrics/__init__.py +0 -56
  917. synth_ai/task/rubrics/loaders.py +0 -152
  918. synth_ai/task/server.py +0 -432
  919. synth_ai/task/trace_correlation_helpers.py +0 -315
  920. synth_ai/task/tracing_utils.py +0 -84
  921. synth_ai/task/validators.py +0 -418
  922. synth_ai/tracing_v3/__init__.py +0 -97
  923. synth_ai/tracing_v3/abstractions.py +0 -302
  924. synth_ai/tracing_v3/config.py +0 -84
  925. synth_ai/tracing_v3/db_config.py +0 -194
  926. synth_ai/tracing_v3/decorators.py +0 -398
  927. synth_ai/tracing_v3/llm_call_record_helpers.py +0 -391
  928. synth_ai/tracing_v3/migration_helper.py +0 -120
  929. synth_ai/tracing_v3/session_tracer.py +0 -540
  930. synth_ai/tracing_v3/storage/base.py +0 -210
  931. synth_ai/tracing_v3/storage/config.py +0 -75
  932. synth_ai/tracing_v3/storage/factory.py +0 -39
  933. synth_ai/tracing_v3/trace_utils.py +0 -317
  934. synth_ai/tracing_v3/turso/daemon.py +0 -151
  935. synth_ai/tracing_v3/turso/models.py +0 -469
  936. synth_ai/tracing_v3/turso/native_manager.py +0 -1209
  937. synth_ai/tracing_v3/utils.py +0 -108
  938. synth_ai/tui/__init__.py +0 -5
  939. synth_ai/tui/__main__.py +0 -13
  940. synth_ai/tui/cli/__init__.py +0 -1
  941. synth_ai/tui/cli/query_experiments.py +0 -164
  942. synth_ai/tui/cli/query_experiments_v3.py +0 -164
  943. synth_ai/tui/dashboard.py +0 -906
  944. synth_ai/v0/api/__init__.py +0 -8
  945. synth_ai/v0/api/models/__init__.py +0 -8
  946. synth_ai/v0/api/models/supported.py +0 -8
  947. synth_ai/v0/config/__init__.py +0 -15
  948. synth_ai/v0/config/base_url.py +0 -12
  949. synth_ai/v0/lm/__init__.py +0 -51
  950. synth_ai/v0/lm/caching/__init__.py +0 -0
  951. synth_ai/v0/lm/caching/constants.py +0 -6
  952. synth_ai/v0/lm/caching/dbs.py +0 -0
  953. synth_ai/v0/lm/caching/ephemeral.py +0 -100
  954. synth_ai/v0/lm/caching/handler.py +0 -137
  955. synth_ai/v0/lm/caching/initialize.py +0 -11
  956. synth_ai/v0/lm/caching/persistent.py +0 -114
  957. synth_ai/v0/lm/config.py +0 -115
  958. synth_ai/v0/lm/constants.py +0 -32
  959. synth_ai/v0/lm/core/__init__.py +0 -8
  960. synth_ai/v0/lm/core/all.py +0 -73
  961. synth_ai/v0/lm/core/exceptions.py +0 -5
  962. synth_ai/v0/lm/core/main.py +0 -331
  963. synth_ai/v0/lm/core/main_v3.py +0 -594
  964. synth_ai/v0/lm/core/synth_models.py +0 -35
  965. synth_ai/v0/lm/core/vendor_clients.py +0 -190
  966. synth_ai/v0/lm/cost/__init__.py +0 -0
  967. synth_ai/v0/lm/cost/monitor.py +0 -1
  968. synth_ai/v0/lm/cost/statefulness.py +0 -1
  969. synth_ai/v0/lm/injection.py +0 -80
  970. synth_ai/v0/lm/overrides.py +0 -206
  971. synth_ai/v0/lm/provider_support/__init__.py +0 -8
  972. synth_ai/v0/lm/provider_support/anthropic.py +0 -972
  973. synth_ai/v0/lm/provider_support/openai.py +0 -1139
  974. synth_ai/v0/lm/provider_support/suppress_logging.py +0 -31
  975. synth_ai/v0/lm/structured_outputs/__init__.py +0 -0
  976. synth_ai/v0/lm/structured_outputs/handler.py +0 -440
  977. synth_ai/v0/lm/structured_outputs/inject.py +0 -297
  978. synth_ai/v0/lm/structured_outputs/rehabilitate.py +0 -185
  979. synth_ai/v0/lm/tools/__init__.py +0 -3
  980. synth_ai/v0/lm/tools/base.py +0 -172
  981. synth_ai/v0/lm/unified_interface.py +0 -202
  982. synth_ai/v0/lm/vendors/__init__.py +0 -0
  983. synth_ai/v0/lm/vendors/base.py +0 -81
  984. synth_ai/v0/lm/vendors/core/__init__.py +0 -0
  985. synth_ai/v0/lm/vendors/core/anthropic_api.py +0 -387
  986. synth_ai/v0/lm/vendors/core/gemini_api.py +0 -292
  987. synth_ai/v0/lm/vendors/core/mistral_api.py +0 -322
  988. synth_ai/v0/lm/vendors/core/openai_api.py +0 -227
  989. synth_ai/v0/lm/vendors/core/synth_dev_api.py +0 -0
  990. synth_ai/v0/lm/vendors/local/__init__.py +0 -0
  991. synth_ai/v0/lm/vendors/local/ollama.py +0 -0
  992. synth_ai/v0/lm/vendors/openai_standard.py +0 -782
  993. synth_ai/v0/lm/vendors/openai_standard_responses.py +0 -259
  994. synth_ai/v0/lm/vendors/retries.py +0 -22
  995. synth_ai/v0/lm/vendors/supported/__init__.py +0 -0
  996. synth_ai/v0/lm/vendors/supported/custom_endpoint.py +0 -415
  997. synth_ai/v0/lm/vendors/supported/deepseek.py +0 -69
  998. synth_ai/v0/lm/vendors/supported/grok.py +0 -75
  999. synth_ai/v0/lm/vendors/supported/groq.py +0 -16
  1000. synth_ai/v0/lm/vendors/supported/ollama.py +0 -15
  1001. synth_ai/v0/lm/vendors/supported/openrouter.py +0 -74
  1002. synth_ai/v0/lm/vendors/supported/together.py +0 -11
  1003. synth_ai/v0/lm/vendors/synth_client.py +0 -835
  1004. synth_ai/v0/lm/warmup.py +0 -186
  1005. synth_ai/v0/tracing/__init__.py +0 -0
  1006. synth_ai/v0/tracing/abstractions.py +0 -224
  1007. synth_ai/v0/tracing/base_client.py +0 -91
  1008. synth_ai/v0/tracing/client_manager.py +0 -131
  1009. synth_ai/v0/tracing/config.py +0 -142
  1010. synth_ai/v0/tracing/context.py +0 -146
  1011. synth_ai/v0/tracing/decorators.py +0 -682
  1012. synth_ai/v0/tracing/events/__init__.py +0 -0
  1013. synth_ai/v0/tracing/events/manage.py +0 -147
  1014. synth_ai/v0/tracing/events/scope.py +0 -86
  1015. synth_ai/v0/tracing/events/store.py +0 -228
  1016. synth_ai/v0/tracing/immediate_client.py +0 -151
  1017. synth_ai/v0/tracing/local.py +0 -18
  1018. synth_ai/v0/tracing/log_client_base.py +0 -73
  1019. synth_ai/v0/tracing/retry_queue.py +0 -186
  1020. synth_ai/v0/tracing/trackers.py +0 -515
  1021. synth_ai/v0/tracing/upload.py +0 -409
  1022. synth_ai/v0/tracing/utils.py +0 -9
  1023. synth_ai/v0/tracing_v1/__init__.py +0 -16
  1024. synth_ai/v0/tracing_v1/abstractions.py +0 -224
  1025. synth_ai/v0/tracing_v1/base_client.py +0 -91
  1026. synth_ai/v0/tracing_v1/client_manager.py +0 -131
  1027. synth_ai/v0/tracing_v1/config.py +0 -142
  1028. synth_ai/v0/tracing_v1/context.py +0 -146
  1029. synth_ai/v0/tracing_v1/decorators.py +0 -703
  1030. synth_ai/v0/tracing_v1/events/__init__.py +0 -0
  1031. synth_ai/v0/tracing_v1/events/manage.py +0 -147
  1032. synth_ai/v0/tracing_v1/events/scope.py +0 -86
  1033. synth_ai/v0/tracing_v1/events/store.py +0 -228
  1034. synth_ai/v0/tracing_v1/immediate_client.py +0 -151
  1035. synth_ai/v0/tracing_v1/local.py +0 -18
  1036. synth_ai/v0/tracing_v1/log_client_base.py +0 -73
  1037. synth_ai/v0/tracing_v1/retry_queue.py +0 -186
  1038. synth_ai/v0/tracing_v1/trackers.py +0 -515
  1039. synth_ai/v0/tracing_v1/upload.py +0 -527
  1040. synth_ai/v0/tracing_v1/utils.py +0 -9
  1041. synth_ai/v0/tracing_v3/__init__.py +0 -10
  1042. synth_ai/v0/tracing_v3/abstractions.py +0 -3
  1043. synth_ai/v0/tracing_v3/decorators.py +0 -3
  1044. synth_ai/v0/tracing_v3/llm_call_record_helpers.py +0 -3
  1045. synth_ai/v0/tracing_v3/session_tracer.py +0 -3
  1046. synth_ai-0.2.14.dist-info/METADATA +0 -139
  1047. synth_ai-0.2.14.dist-info/RECORD +0 -762
  1048. synth_ai-0.2.14.dist-info/top_level.txt +0 -2
  1049. /synth_ai/{demos/demo_task_apps → cli/demo_apps}/crafter/__init__.py +0 -0
  1050. /synth_ai/{demos → cli/demo_apps}/demo_task_apps/__init__.py +0 -0
  1051. /synth_ai/{demos → cli/demo_apps}/demo_task_apps/crafter/configs/crafter_fft_4b.toml +0 -0
  1052. /synth_ai/{demos → cli/demo_apps}/demo_task_apps/crafter/configs/rl_from_base_qwen4b.toml +0 -0
  1053. /synth_ai/{demos → cli/demo_apps}/demo_task_apps/math/__init__.py +0 -0
  1054. /synth_ai/{demos → cli/demo_apps}/demo_task_apps/math/_common.py +0 -0
  1055. /synth_ai/{demos → cli/demo_apps}/demo_task_apps/math/app.py +0 -0
  1056. /synth_ai/{demos → cli/demo_apps}/demo_task_apps/math/config.toml +0 -0
  1057. /synth_ai/{demos → cli/demo_apps}/demo_task_apps/math/deploy_modal.py +0 -0
  1058. {examples/task_apps → synth_ai/core/apps}/__init__.py +0 -0
  1059. /synth_ai/{tracing_v3 → core/tracing_v3}/examples/basic_usage.py +0 -0
  1060. /synth_ai/{tracing_v3 → core/tracing_v3}/hooks.py +0 -0
  1061. /synth_ai/{tracing_v3 → core/tracing_v3}/lm_call_record_abstractions.py +0 -0
  1062. /synth_ai/{tracing_v3 → core/tracing_v3}/replica_sync.py +0 -0
  1063. /synth_ai/{tracing_v3 → core/tracing_v3}/serialization.py +0 -0
  1064. /synth_ai/{tracing_v3 → core/tracing_v3}/storage/__init__.py +0 -0
  1065. /synth_ai/{tracing_v3 → core/tracing_v3}/storage/exceptions.py +0 -0
  1066. /synth_ai/{tracing_v3 → core/tracing_v3}/storage/types.py +0 -0
  1067. /synth_ai/{tracing_v3 → core/tracing_v3}/storage/utils.py +0 -0
  1068. /synth_ai/{tracing_v3 → core/tracing_v3}/turso/__init__.py +0 -0
  1069. /synth_ai/{evals → sdk/judging}/types.py +0 -0
  1070. /synth_ai/{learning → sdk/learning}/algorithms.py +0 -0
  1071. /synth_ai/{learning → sdk/learning}/config.py +0 -0
  1072. /synth_ai/{learning → sdk/learning}/constants.py +0 -0
  1073. /synth_ai/{learning → sdk/learning}/core.py +0 -0
  1074. /synth_ai/{learning → sdk/learning}/gateway.py +0 -0
  1075. /synth_ai/{learning → sdk/learning}/rl/__init__.py +0 -0
  1076. /synth_ai/{learning → sdk/learning}/rl/config.py +0 -0
  1077. /synth_ai/{learning → sdk/learning}/rl_client.py +0 -0
  1078. /synth_ai/{learning → sdk/learning}/sft/__init__.py +0 -0
  1079. /synth_ai/{learning → sdk/learning}/sse.py +0 -0
  1080. /synth_ai/{task → sdk/task}/auth.py +0 -0
  1081. /synth_ai/{task → sdk/task}/client.py +0 -0
  1082. /synth_ai/{task → sdk/task}/errors.py +0 -0
  1083. /synth_ai/{task → sdk/task}/health.py +0 -0
  1084. /synth_ai/{task → sdk/task}/json.py +0 -0
  1085. /synth_ai/{task → sdk/task}/rubrics/models.py +0 -0
  1086. /synth_ai/{task → sdk/task}/rubrics/scoring.py +0 -0
  1087. /synth_ai/{task → sdk/task}/rubrics/strict.py +0 -0
  1088. /synth_ai/{task → sdk/task}/vendors.py +0 -0
  1089. {synth_ai-0.2.14.dist-info → synth_ai-0.4.1.dist-info}/WHEEL +0 -0
  1090. {synth_ai-0.2.14.dist-info → synth_ai-0.4.1.dist-info}/entry_points.txt +0 -0
  1091. {synth_ai-0.2.14.dist-info → synth_ai-0.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,3153 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import ast
5
+ import asyncio
6
+ import contextlib
7
+ import functools
8
+ import hashlib
9
+ import importlib
10
+ import importlib.util
11
+ import inspect
12
+ import os
13
+ import shlex
14
+ import shutil
15
+ import signal
16
+ import subprocess
17
+ import sys
18
+ import tempfile
19
+ import textwrap
20
+ import time
21
+ import types
22
+ from collections.abc import Callable, Iterable, Iterator, Sequence
23
+ from dataclasses import dataclass
24
+ from datetime import UTC, datetime
25
+ from pathlib import Path
26
+ from typing import Any, cast
27
+
28
+ try: # Python 3.11+
29
+ import tomllib as _toml
30
+ except Exception: # pragma: no cover - fallback
31
+ _toml = None # type: ignore
32
+
33
+ import click
34
+ from click.exceptions import Abort
35
+
36
+ from synth_ai.cli.commands.eval import core as eval_core
37
+ from synth_ai.cli.commands.filter import core as filter_core
38
+
39
+ # Tracing imports - make conditional for optional dependencies
40
+ try:
41
+ from synth_ai.core.tracing_v3 import ( # type: ignore[import-untyped]
42
+ BaseEvent,
43
+ EnvironmentEvent,
44
+ RuntimeEvent,
45
+ SessionEventMarkovBlanketMessage,
46
+ SessionMessageContent,
47
+ SessionTimeStep,
48
+ SessionTracer,
49
+ TimeRecord,
50
+ )
51
+ from synth_ai.core.tracing_v3 import ( # type: ignore[import-untyped]
52
+ SessionTrace as V3SessionTrace,
53
+ )
54
+ _TRACING_AVAILABLE = True
55
+ except (ImportError, ModuleNotFoundError, TypeError):
56
+ # Tracing system not available (missing optional dependencies)
57
+ BaseEvent = EnvironmentEvent = RuntimeEvent = None # type: ignore
58
+ SessionEventMarkovBlanketMessage = SessionMessageContent = None # type: ignore
59
+ SessionTimeStep = SessionTracer = TimeRecord = None # type: ignore
60
+ V3SessionTrace = None # type: ignore
61
+ _TRACING_AVAILABLE = False
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # Dynamic imports to avoid hard dependencies during type checking.
65
+ # ---------------------------------------------------------------------------
66
+ ModalDeploymentConfigType = TaskAppConfigType = TaskAppEntryType = Any
67
+
68
+ try: # Resolve base URL defaults lazily
69
+ _config_module = cast(
70
+ Any, importlib.import_module("synth_ai.core.env")
71
+ )
72
+ PROD_BASE_URL_DEFAULT = cast(str, _config_module.PROD_BASE_URL_DEFAULT)
73
+ except Exception: # pragma: no cover - fallback
74
+ PROD_BASE_URL_DEFAULT = "https://api.usesynth.ai"
75
+
76
+ try:
77
+ _task_apps_module = cast(Any, importlib.import_module("synth_ai.sdk.task.apps"))
78
+ ModalDeploymentConfig = cast(
79
+ type[ModalDeploymentConfigType], _task_apps_module.ModalDeploymentConfig
80
+ )
81
+ TaskAppConfig = cast(type[TaskAppConfigType], _task_apps_module.TaskAppConfig)
82
+ TaskAppEntry = cast(type[TaskAppEntryType], _task_apps_module.TaskAppEntry)
83
+ registry = _task_apps_module.registry
84
+ except Exception as exc: # pragma: no cover - critical dependency
85
+ raise RuntimeError("Unable to load task app registry") from exc
86
+
87
+ try:
88
+ _task_server_module = cast(Any, importlib.import_module("synth_ai.sdk.task.server"))
89
+ create_task_app = cast(Callable[..., Any], _task_server_module.create_task_app)
90
+ run_task_app = cast(Callable[..., Any], _task_server_module.run_task_app)
91
+ except Exception as exc: # pragma: no cover - critical dependency
92
+ raise RuntimeError("Unable to load task app server utilities") from exc
93
+
94
+
95
+ def _load_demo_directory() -> Path | None:
96
+ """Return the demo task apps directory if available."""
97
+
98
+ try:
99
+ module = cast(
100
+ Any, importlib.import_module("synth_ai.cli.demo_apps.demo_task_apps.core")
101
+ )
102
+ loader = cast(Callable[[], str | Path | None], module.load_demo_dir)
103
+ demo_dir = loader()
104
+ if isinstance(demo_dir, str | Path):
105
+ demo_path = Path(demo_dir)
106
+ if demo_path.exists():
107
+ return demo_path.resolve()
108
+ except Exception:
109
+ return None
110
+ return None
111
+
112
+
113
+ def _maybe_import(name: str) -> Any:
114
+ """Safely import a module by name and return it, or None on failure."""
115
+
116
+ try:
117
+ return importlib.import_module(name)
118
+ except Exception:
119
+ return None
120
+
121
+ REPO_ROOT = Path(__file__).resolve().parents[2]
122
+
123
+ DEFAULT_IGNORE_DIRS = {
124
+ ".git",
125
+ "__pycache__",
126
+ "node_modules",
127
+ "venv",
128
+ ".venv",
129
+ "build",
130
+ "dist",
131
+ ".mypy_cache",
132
+ ".pytest_cache",
133
+ }
134
+
135
+ DEFAULT_SEARCH_RELATIVE = (
136
+ Path("."),
137
+ Path("examples"),
138
+ Path("synth_ai"),
139
+ )
140
+
141
+
142
+ def _pearson(xs: Sequence[float], ys: Sequence[float]) -> float | None:
143
+ if len(xs) != len(ys) or len(xs) < 2:
144
+ return None
145
+ mean_x = sum(xs) / len(xs)
146
+ mean_y = sum(ys) / len(ys)
147
+ num = 0.0
148
+ denom_x = 0.0
149
+ denom_y = 0.0
150
+ for x, y in zip(xs, ys, strict=False):
151
+ dx = x - mean_x
152
+ dy = y - mean_y
153
+ num += dx * dy
154
+ denom_x += dx * dx
155
+ denom_y += dy * dy
156
+ if denom_x <= 0 or denom_y <= 0:
157
+ return None
158
+ return num / (denom_x ** 0.5 * denom_y ** 0.5)
159
+
160
+
161
+ @dataclass
162
+ class AppChoice:
163
+ app_id: str
164
+ label: str
165
+ path: Path
166
+ source: str
167
+ description: str | None = None
168
+ aliases: tuple[str, ...] = ()
169
+ entry: TaskAppEntryType | None = None
170
+ entry_loader: Callable[[], TaskAppEntryType] | None = None
171
+ modal_script: Path | None = None
172
+ lineno: int | None = None
173
+
174
+ def ensure_entry(self) -> TaskAppEntryType:
175
+ if self.entry is not None:
176
+ return self.entry
177
+ if self.entry_loader is None:
178
+ raise click.ClickException(f"Unable to load task app '{self.app_id}' from {self.path}")
179
+ entry = self.entry_loader()
180
+ self.entry = entry
181
+ return entry
182
+
183
+
184
+ @dataclass
185
+ class JudgeSpec:
186
+ name: str
187
+ fn: Callable[..., Any]
188
+ kwargs: dict[str, Any]
189
+
190
+
191
+ def _parse_datetime_for_trace(value: Any) -> datetime | None:
192
+ if isinstance(value, datetime):
193
+ return value if value.tzinfo else value.replace(tzinfo=UTC)
194
+ if isinstance(value, str):
195
+ value = value.replace("Z", "+00:00")
196
+ try:
197
+ dt = datetime.fromisoformat(value)
198
+ except ValueError:
199
+ try:
200
+ dt = datetime.fromtimestamp(float(value), tz=UTC)
201
+ except Exception:
202
+ return None
203
+ return dt if dt.tzinfo else dt.replace(tzinfo=UTC)
204
+ if isinstance(value, int | float):
205
+ return datetime.fromtimestamp(float(value), tz=UTC)
206
+ return None
207
+
208
+
209
+ def _time_record_from_dict(payload: dict[str, Any] | None) -> TimeRecord:
210
+ payload = payload or {}
211
+ event_time = payload.get("event_time")
212
+ if event_time is None:
213
+ event_time = float(time.time())
214
+ elif not isinstance(event_time, int | float):
215
+ try:
216
+ event_time = float(event_time)
217
+ except Exception:
218
+ event_time = float(time.time())
219
+ message_time = payload.get("message_time")
220
+ if message_time is not None:
221
+ try:
222
+ message_time = int(message_time)
223
+ except Exception:
224
+ message_time = None
225
+ return TimeRecord(event_time=event_time, message_time=message_time)
226
+
227
+
228
+ def _event_from_dict(payload: dict[str, Any]) -> BaseEvent:
229
+ system_instance_id = payload.get("system_instance_id", "")
230
+ time_record = _time_record_from_dict(payload.get("time_record"))
231
+ metadata = payload.get("metadata") or {}
232
+ event_metadata = payload.get("event_metadata")
233
+
234
+ if "actions" in payload:
235
+ return RuntimeEvent(
236
+ system_instance_id=system_instance_id,
237
+ time_record=time_record,
238
+ metadata=metadata,
239
+ event_metadata=event_metadata,
240
+ actions=payload.get("actions") or [],
241
+ )
242
+ if any(key in payload for key in ("reward", "terminated", "truncated")):
243
+ return EnvironmentEvent(
244
+ system_instance_id=system_instance_id,
245
+ time_record=time_record,
246
+ metadata=metadata,
247
+ event_metadata=event_metadata,
248
+ reward=float(payload.get("reward", 0.0) or 0.0),
249
+ terminated=bool(payload.get("terminated", False)),
250
+ truncated=bool(payload.get("truncated", False)),
251
+ system_state_before=payload.get("system_state_before"),
252
+ system_state_after=payload.get("system_state_after"),
253
+ )
254
+ # Check for LM CAIS event fields
255
+ if any(key in payload for key in ("model_name", "provider", "call_records")):
256
+ from synth_ai.core.tracing_v3.abstractions import LMCAISEvent
257
+ # Note: call_records are left as dicts - the storage layer will handle serialization
258
+ call_records = payload.get("call_records") or []
259
+ return LMCAISEvent(
260
+ system_instance_id=system_instance_id,
261
+ time_record=time_record,
262
+ metadata=metadata,
263
+ event_metadata=event_metadata,
264
+ model_name=payload.get("model_name", ""),
265
+ provider=payload.get("provider", ""),
266
+ input_tokens=payload.get("input_tokens"),
267
+ output_tokens=payload.get("output_tokens"),
268
+ total_tokens=payload.get("total_tokens"),
269
+ cost_usd=payload.get("cost_usd"),
270
+ latency_ms=payload.get("latency_ms"),
271
+ span_id=payload.get("span_id"),
272
+ trace_id=payload.get("trace_id"),
273
+ call_records=call_records,
274
+ )
275
+ return BaseEvent(
276
+ system_instance_id=system_instance_id,
277
+ time_record=time_record,
278
+ metadata=metadata,
279
+ event_metadata=event_metadata,
280
+ )
281
+
282
+
283
+ def _markov_message_from_dict(payload: dict[str, Any]) -> SessionEventMarkovBlanketMessage:
284
+ content_payload = payload.get("content") or {}
285
+ content = SessionMessageContent(
286
+ text=content_payload.get("text"),
287
+ json_payload=content_payload.get("json_payload"),
288
+ )
289
+ raw_type = (payload.get("message_type") or "").lower()
290
+ original_type = payload.get("message_type") or raw_type
291
+
292
+ if raw_type in ("observation", "policy_system_prompt"):
293
+ normalized_type = "system"
294
+ elif raw_type in ("action", "policy_tool_call"):
295
+ normalized_type = "assistant"
296
+ elif raw_type in {"user", "assistant", "system", "tool_use", "tool_result"}:
297
+ normalized_type = raw_type
298
+ else:
299
+ normalized_type = "system"
300
+
301
+ metadata = dict(payload.get("metadata") or {})
302
+ metadata["original_message_type"] = original_type
303
+
304
+ return SessionEventMarkovBlanketMessage(
305
+ content=content,
306
+ message_type=normalized_type,
307
+ time_record=_time_record_from_dict(payload.get("time_record")),
308
+ metadata=metadata,
309
+ )
310
+
311
+
312
+ def _step_from_dict(payload: dict[str, Any]) -> SessionTimeStep:
313
+ events = [
314
+ _event_from_dict(event)
315
+ for event in payload.get("events", [])
316
+ if isinstance(event, dict)
317
+ ]
318
+ messages = [
319
+ _markov_message_from_dict(msg)
320
+ for msg in payload.get("markov_blanket_messages", [])
321
+ if isinstance(msg, dict)
322
+ ]
323
+ timestamp = _parse_datetime_for_trace(payload.get("timestamp")) or datetime.now(UTC)
324
+ completed_at = _parse_datetime_for_trace(payload.get("completed_at"))
325
+ return SessionTimeStep(
326
+ step_id=payload.get("step_id", ""),
327
+ step_index=int(payload.get("step_index", 0) or 0),
328
+ timestamp=timestamp,
329
+ turn_number=payload.get("turn_number"),
330
+ events=events,
331
+ markov_blanket_messages=messages,
332
+ step_metadata=payload.get("step_metadata") or {},
333
+ completed_at=completed_at,
334
+ )
335
+
336
+
337
+ def _session_trace_from_dict(payload: dict[str, Any]) -> V3SessionTrace | None:
338
+ if not isinstance(payload, dict):
339
+ return None
340
+ steps = [
341
+ _step_from_dict(step)
342
+ for step in payload.get("session_time_steps", [])
343
+ if isinstance(step, dict)
344
+ ]
345
+ events = [
346
+ _event_from_dict(event)
347
+ for event in payload.get("event_history", [])
348
+ if isinstance(event, dict)
349
+ ]
350
+ markov_history = [
351
+ _markov_message_from_dict(msg)
352
+ for msg in payload.get("markov_blanket_message_history", [])
353
+ if isinstance(msg, dict)
354
+ ]
355
+ created_at = _parse_datetime_for_trace(payload.get("created_at")) or datetime.now(UTC)
356
+ metadata = payload.get("metadata") or {}
357
+ session_metadata = payload.get("session_metadata")
358
+ return V3SessionTrace(
359
+ session_id=payload.get("session_id", ""),
360
+ created_at=created_at,
361
+ session_time_steps=steps,
362
+ event_history=events,
363
+ markov_blanket_message_history=markov_history,
364
+ metadata=metadata,
365
+ session_metadata=session_metadata,
366
+ )
367
+
368
+
369
+ async def _store_trace(
370
+ tracer: SessionTracer | None,
371
+ trace_namespace: dict[str, Any] | None,
372
+ extra_metadata: dict[str, Any] | None = None,
373
+ ):
374
+ import logging
375
+ _logger = logging.getLogger(__name__)
376
+
377
+ _logger.info(f"[STORE_TRACE_DEBUG] Called with tracer={tracer is not None}, trace_namespace={trace_namespace is not None}")
378
+
379
+ if tracer is None or not isinstance(trace_namespace, dict):
380
+ _logger.warning(f"[STORE_TRACE_DEBUG] Early return: tracer={tracer is not None}, trace_namespace type={type(trace_namespace)}")
381
+ return
382
+
383
+ _logger.info(f"[STORE_TRACE_DEBUG] trace_namespace keys: {list(trace_namespace.keys())}")
384
+
385
+ # Handle both formats:
386
+ # - With session_trace key: {"session_trace": {...}}
387
+ # - Without session_trace key (trace itself is the session): {"session_id": ..., "markov_blanket_message_history": ...}
388
+ session_payload = trace_namespace.get("session_trace")
389
+ if not isinstance(session_payload, dict):
390
+ # If no session_trace key, assume "full" format where trace itself is the session_trace
391
+ if "session_id" in trace_namespace:
392
+ session_payload = trace_namespace
393
+ _logger.info("[STORE_TRACE_DEBUG] Using trace_namespace directly as session_payload (no session_trace key)")
394
+ else:
395
+ _logger.warning(f"[STORE_TRACE_DEBUG] No session_trace found or wrong type: {type(session_payload)}")
396
+ return
397
+
398
+ _logger.info(f"[STORE_TRACE_DEBUG] session_payload keys: {list(session_payload.keys())}")
399
+ msg_count = len(session_payload.get("markov_blanket_message_history", []))
400
+ _logger.info(f"[STORE_TRACE_DEBUG] Found {msg_count} messages in session_payload")
401
+
402
+ trace_obj = _session_trace_from_dict(session_payload)
403
+ if trace_obj is None:
404
+ _logger.warning("[STORE_TRACE_DEBUG] _session_trace_from_dict returned None")
405
+ return
406
+
407
+ _logger.info(f"[STORE_TRACE_DEBUG] Created SessionTrace object with {len(trace_obj.markov_blanket_message_history)} messages")
408
+
409
+ if tracer.db is None:
410
+ await tracer.initialize()
411
+ meta = dict(trace_obj.metadata or {})
412
+ if extra_metadata:
413
+ meta.update(extra_metadata)
414
+ trace_obj.metadata = meta
415
+
416
+ _logger.info(f"[STORE_TRACE_DEBUG] Calling insert_session_trace for session_id={trace_obj.session_id}")
417
+ await tracer.db.insert_session_trace(trace_obj) # type: ignore[attr-defined]
418
+ _logger.info("[STORE_TRACE_DEBUG] Successfully inserted trace")
419
+
420
+ def _temporary_sys_path(paths: Sequence[Path]):
421
+ """Context manager to prepend entries to sys.path temporarily."""
422
+
423
+ @contextlib.contextmanager
424
+ def _manager() -> Iterator[None]:
425
+ added: list[str] = []
426
+ for p in paths:
427
+ try:
428
+ resolved = str(p.resolve())
429
+ except Exception:
430
+ continue
431
+ if resolved in sys.path:
432
+ continue
433
+ sys.path.insert(0, resolved)
434
+ added.append(resolved)
435
+ try:
436
+ yield None
437
+ finally:
438
+ for entry in added:
439
+ with contextlib.suppress(ValueError):
440
+ sys.path.remove(entry)
441
+
442
+ return _manager()
443
+
444
+
445
+ def _possible_module_names(
446
+ path: Path, module_search_roots: Sequence[Path]
447
+ ) -> list[tuple[str, Path]]:
448
+ """Return potential module names based on candidate roots."""
449
+
450
+ candidates: list[tuple[str, Path]] = []
451
+ for root in module_search_roots:
452
+ try:
453
+ resolved_root = root.resolve()
454
+ except Exception:
455
+ continue
456
+ if not resolved_root.exists() or not path.is_relative_to(resolved_root):
457
+ continue
458
+ relative = path.relative_to(resolved_root)
459
+ stem = relative.with_suffix("")
460
+ parts = list(stem.parts)
461
+ if not parts:
462
+ continue
463
+ module_name = ".".join(parts)
464
+ if module_name:
465
+ candidates.append((module_name, resolved_root))
466
+ return candidates
467
+
468
+
469
+ def _ensure_parent_namespace(module_name: str, search_root: Path) -> None:
470
+ """Ensure namespace packages exist for dotted module names."""
471
+
472
+ parts = module_name.split(".")
473
+ for depth in range(1, len(parts)):
474
+ parent_name = ".".join(parts[:depth])
475
+ if parent_name in sys.modules:
476
+ continue
477
+ parent_module = types.ModuleType(parent_name)
478
+ candidate_dir = search_root.joinpath(*parts[:depth])
479
+ try:
480
+ resolved = candidate_dir.resolve()
481
+ except Exception:
482
+ resolved = search_root.resolve()
483
+ parent_module.__path__ = [str(resolved)] # type: ignore[attr-defined]
484
+ sys.modules[parent_name] = parent_module
485
+
486
+
487
+ def _should_ignore_path(path: Path) -> bool:
488
+ return any(part in DEFAULT_IGNORE_DIRS for part in path.parts)
489
+
490
+
491
+ def _candidate_search_roots() -> list[Path]:
492
+ """Only search for task apps in the current working directory and subdirectories."""
493
+ roots: list[Path] = []
494
+
495
+ demo_path = _load_demo_directory()
496
+ if demo_path is not None and demo_path.is_dir():
497
+ roots.append(demo_path)
498
+
499
+ # Allow explicit search paths via environment variable
500
+ env_paths = os.environ.get("SYNTH_TASK_APP_SEARCH_PATH")
501
+ if env_paths:
502
+ for chunk in env_paths.split(os.pathsep):
503
+ if chunk:
504
+ roots.append(Path(chunk).expanduser())
505
+
506
+ # Always include current working directory
507
+ cwd = Path.cwd().resolve()
508
+ roots.append(cwd)
509
+
510
+ for rel in DEFAULT_SEARCH_RELATIVE:
511
+ try:
512
+ candidate = (cwd / rel).resolve()
513
+ except Exception:
514
+ continue
515
+ roots.append(candidate)
516
+
517
+ # Remove duplicates while preserving order
518
+ seen: set[Path] = set()
519
+ ordered: list[Path] = []
520
+ for root in roots:
521
+ try:
522
+ resolved = root.resolve()
523
+ except Exception:
524
+ continue
525
+ if resolved in seen or not resolved.exists():
526
+ continue
527
+ seen.add(resolved)
528
+ ordered.append(resolved)
529
+ return ordered
530
+
531
+
532
+ class _TaskAppConfigVisitor(ast.NodeVisitor):
533
+ def __init__(self) -> None:
534
+ self.matches: list[tuple[str, int]] = []
535
+
536
+ def visit_Call(self, node: ast.Call) -> None: # noqa: D401
537
+ if _is_task_app_config_call(node):
538
+ app_id = _extract_app_id(node)
539
+ if app_id:
540
+ self.matches.append((app_id, getattr(node, "lineno", 0)))
541
+ elif _is_register_task_app_call(node):
542
+ app_id = _extract_register_app_id(node)
543
+ if app_id:
544
+ self.matches.append((app_id, getattr(node, "lineno", 0)))
545
+ self.generic_visit(node)
546
+
547
+
548
+ def _is_task_app_config_call(node: ast.Call) -> bool:
549
+ func = node.func
550
+ return (isinstance(func, ast.Name) and func.id == "TaskAppConfig") or (
551
+ isinstance(func, ast.Attribute) and func.attr == "TaskAppConfig"
552
+ )
553
+
554
+
555
+ def _extract_app_id(node: ast.Call) -> str | None:
556
+ for kw in node.keywords:
557
+ if (
558
+ kw.arg == "app_id"
559
+ and isinstance(kw.value, ast.Constant)
560
+ and isinstance(kw.value.value, str)
561
+ ):
562
+ return kw.value.value
563
+ if node.args:
564
+ first = node.args[0]
565
+ if isinstance(first, ast.Constant) and isinstance(first.value, str):
566
+ return first.value
567
+ return None
568
+
569
+
570
+ def _is_register_task_app_call(node: ast.Call) -> bool:
571
+ func = node.func
572
+ return (isinstance(func, ast.Name) and func.id == "register_task_app") or (
573
+ isinstance(func, ast.Attribute) and func.attr == "register_task_app"
574
+ )
575
+
576
+
577
+ def _extract_register_app_id(node: ast.Call) -> str | None:
578
+ # Look for entry=TaskAppEntry(app_id="...", ...)
579
+ for kw in node.keywords:
580
+ if kw.arg == "entry" and isinstance(kw.value, ast.Call):
581
+ entry_call = kw.value
582
+ if isinstance(entry_call.func, ast.Name) and entry_call.func.id == "TaskAppEntry":
583
+ for entry_kw in entry_call.keywords:
584
+ if (
585
+ entry_kw.arg == "app_id"
586
+ and isinstance(entry_kw.value, ast.Constant)
587
+ and isinstance(entry_kw.value.value, str)
588
+ ):
589
+ return entry_kw.value.value
590
+ return None
591
+
592
+
593
+ class _ModalAppVisitor(ast.NodeVisitor):
594
+ def __init__(self) -> None:
595
+ self.app_aliases: set[str] = set()
596
+ self.modal_aliases: set[str] = set()
597
+ self.matches: list[tuple[str, int]] = []
598
+
599
+ def visit_ImportFrom(self, node: ast.ImportFrom) -> None: # noqa: D401
600
+ if node.module == "modal":
601
+ for alias in node.names:
602
+ if alias.name == "App":
603
+ self.app_aliases.add(alias.asname or alias.name)
604
+ self.generic_visit(node)
605
+
606
+ def visit_Import(self, node: ast.Import) -> None: # noqa: D401
607
+ for alias in node.names:
608
+ if alias.name == "modal":
609
+ self.modal_aliases.add(alias.asname or alias.name)
610
+ self.generic_visit(node)
611
+
612
+ def visit_Call(self, node: ast.Call) -> None: # noqa: D401
613
+ func = node.func
614
+ if isinstance(func, ast.Name) and func.id in self.app_aliases:
615
+ name = _extract_modal_app_name(node)
616
+ if name:
617
+ self.matches.append((name, getattr(node, "lineno", 0)))
618
+ elif isinstance(func, ast.Attribute):
619
+ if (
620
+ isinstance(func.value, ast.Name)
621
+ and func.value.id in self.modal_aliases
622
+ and func.attr == "App"
623
+ ):
624
+ name = _extract_modal_app_name(node)
625
+ if name:
626
+ self.matches.append((name, getattr(node, "lineno", 0)))
627
+ self.generic_visit(node)
628
+
629
+
630
+ def _extract_modal_app_name(node: ast.Call) -> str | None:
631
+ for kw in node.keywords:
632
+ if (
633
+ kw.arg in {"name", "app_name"}
634
+ and isinstance(kw.value, ast.Constant)
635
+ and isinstance(kw.value.value, str)
636
+ ):
637
+ return kw.value.value
638
+ if node.args:
639
+ first = node.args[0]
640
+ if isinstance(first, ast.Constant) and isinstance(first.value, str):
641
+ return first.value
642
+ return None
643
+
644
+
645
+ def _collect_task_app_choices() -> list[AppChoice]:
646
+ # Clear registry to avoid duplicate registration errors
647
+ registry.clear()
648
+
649
+ choices: list[AppChoice] = []
650
+ with contextlib.suppress(Exception):
651
+ _maybe_import("synth_ai.cli.demo_apps.demo_task_apps")
652
+ # Only use discovered task apps, not registered ones (since we moved them to examples)
653
+ choices.extend(_collect_scanned_task_configs())
654
+ choices.extend(_collect_modal_scripts())
655
+
656
+ unique: dict[tuple[str, Path], AppChoice] = {}
657
+ ordered: list[AppChoice] = []
658
+ for choice in choices:
659
+ key = (choice.app_id, choice.path.resolve())
660
+ if key in unique:
661
+ existing = unique[key]
662
+ if existing.source == "registered" and choice.source != "registered":
663
+ continue
664
+ if choice.source == "registered" and existing.source != "registered":
665
+ unique[key] = choice
666
+ idx = ordered.index(existing)
667
+ ordered[idx] = choice
668
+ continue
669
+ unique[key] = choice
670
+ ordered.append(choice)
671
+ ordered.sort(key=_app_choice_sort_key)
672
+ return ordered
673
+
674
+
675
+ def _collect_registered_choices() -> list[AppChoice]:
676
+ result: list[AppChoice] = []
677
+ for entry in registry.list(): # type: ignore[attr-defined]
678
+ module_name = entry.config_factory.__module__
679
+ module = sys.modules.get(module_name)
680
+ if module is None:
681
+ module = importlib.import_module(module_name)
682
+ module_file = getattr(module, "__file__", None)
683
+ path = Path(module_file).resolve() if module_file else REPO_ROOT
684
+ result.append(
685
+ AppChoice(
686
+ app_id=entry.app_id,
687
+ label=entry.app_id,
688
+ path=path,
689
+ source="registered",
690
+ description=entry.description,
691
+ aliases=tuple(entry.aliases),
692
+ entry=entry,
693
+ )
694
+ )
695
+ return result
696
+
697
+
698
+ def _collect_scanned_task_configs() -> list[AppChoice]:
699
+ results: list[AppChoice] = []
700
+ seen: set[tuple[str, Path]] = set()
701
+ for root in _candidate_search_roots():
702
+ try:
703
+ root_resolved = root.resolve()
704
+ except Exception:
705
+ continue
706
+ if not root.exists() or not root.is_dir():
707
+ continue
708
+ for path in root.rglob("*.py"):
709
+ if not path.is_file():
710
+ continue
711
+ if _should_ignore_path(path):
712
+ continue
713
+ try:
714
+ source = path.read_text(encoding="utf-8")
715
+ except Exception:
716
+ continue
717
+ try:
718
+ tree = ast.parse(source, filename=str(path))
719
+ except SyntaxError:
720
+ continue
721
+ visitor = _TaskAppConfigVisitor()
722
+ visitor.visit(tree)
723
+ for app_id, lineno in visitor.matches:
724
+ key = (app_id, path.resolve())
725
+ if key in seen:
726
+ continue
727
+ seen.add(key)
728
+ results.append(
729
+ AppChoice(
730
+ app_id=app_id,
731
+ label=app_id,
732
+ path=path.resolve(),
733
+ source="discovered",
734
+ description=f"TaskAppConfig in {path.name} (line {lineno})",
735
+ entry_loader=lambda p=path.resolve(),
736
+ a=app_id,
737
+ roots=(root_resolved,): _load_entry_from_path(
738
+ p, a, module_search_roots=roots
739
+ ),
740
+ lineno=lineno,
741
+ )
742
+ )
743
+ return results
744
+
745
+
746
+ def _collect_modal_scripts() -> list[AppChoice]:
747
+ results: list[AppChoice] = []
748
+ seen: set[tuple[str, Path]] = set()
749
+ for root in _candidate_search_roots():
750
+ if not root.exists() or not root.is_dir():
751
+ continue
752
+ for path in root.rglob("*.py"):
753
+ if not path.is_file():
754
+ continue
755
+ if _should_ignore_path(path):
756
+ continue
757
+ try:
758
+ source = path.read_text(encoding="utf-8")
759
+ except Exception:
760
+ continue
761
+ try:
762
+ tree = ast.parse(source, filename=str(path))
763
+ except SyntaxError:
764
+ continue
765
+ visitor = _ModalAppVisitor()
766
+ visitor.visit(tree)
767
+ for app_name, lineno in visitor.matches:
768
+ key = (app_name, path.resolve())
769
+ if key in seen:
770
+ continue
771
+ seen.add(key)
772
+ results.append(
773
+ AppChoice(
774
+ app_id=app_name,
775
+ label=app_name,
776
+ path=path.resolve(),
777
+ source="modal-script",
778
+ description=f"Modal App '{app_name}' in {path.name} (line {lineno})",
779
+ modal_script=path.resolve(),
780
+ lineno=lineno,
781
+ )
782
+ )
783
+ return results
784
+
785
+
786
+ def _app_choice_sort_key(choice: AppChoice) -> tuple[int, int, int, int, int, str, str]:
787
+ """Ranking heuristic so wrapper-style task apps surface first."""
788
+
789
+ # Prioritize apps in the current working directory (demo or otherwise)
790
+ cwd_rank = 1
791
+ try:
792
+ cwd = Path.cwd().resolve()
793
+ if choice.path.is_relative_to(cwd):
794
+ # Check if this is directly in CWD (not in subdirectories like examples/)
795
+ try:
796
+ rel_path = choice.path.relative_to(cwd)
797
+ # If it's in the immediate directory or one level deep, prioritize it
798
+ if len(rel_path.parts) <= 2:
799
+ cwd_rank = 0
800
+ except Exception:
801
+ pass
802
+ except Exception:
803
+ pass
804
+
805
+ # Further prioritize apps in the demo directory if one is set
806
+ demo_rank = 1
807
+ demo_dir = _load_demo_directory()
808
+ if demo_dir and choice.path.is_relative_to(demo_dir):
809
+ demo_rank = 0
810
+
811
+ modal_rank = 1 if choice.modal_script else 0
812
+
813
+ name = choice.path.name.lower()
814
+ file_rank = 3
815
+ if name.endswith("_task_app.py") or name.endswith("task_app.py"):
816
+ file_rank = 0
817
+ elif name.endswith("_app.py") or "task_app" in name:
818
+ file_rank = 1
819
+ elif name.endswith(".py"):
820
+ file_rank = 2
821
+
822
+ directory_rank = 0 if choice.path.parent.name.lower() in {"task_app", "task_apps"} else 1
823
+
824
+ return (
825
+ demo_rank,
826
+ cwd_rank,
827
+ modal_rank,
828
+ file_rank,
829
+ directory_rank,
830
+ choice.app_id,
831
+ str(choice.path),
832
+ )
833
+
834
+
835
+ def _choice_matches_identifier(choice: AppChoice, identifier: str) -> bool:
836
+ ident = identifier.strip()
837
+ if not ident:
838
+ return False
839
+ return ident == choice.app_id or ident == choice.label or ident in choice.aliases
840
+
841
+
842
+ def _choice_has_modal_support(choice: AppChoice) -> bool:
843
+ if choice.modal_script:
844
+ return True
845
+ try:
846
+ entry = choice.ensure_entry()
847
+ except click.ClickException:
848
+ # If we can't load the entry, try to detect Modal support via AST parsing
849
+ return _has_modal_support_in_file(choice.path)
850
+ return entry.modal is not None # type: ignore[attr-defined]
851
+
852
+
853
+ def _has_modal_support_in_file(path: Path) -> bool:
854
+ """Detect if a file has Modal deployment support by parsing the AST."""
855
+ try:
856
+ source = path.read_text(encoding="utf-8")
857
+ tree = ast.parse(source, filename=str(path))
858
+
859
+ # Look for ModalDeploymentConfig in register_task_app calls
860
+ for node in ast.walk(tree):
861
+ if isinstance(node, ast.Call) and _is_register_task_app_call(node):
862
+ # Check if the entry has modal=ModalDeploymentConfig(...)
863
+ for kw in node.keywords:
864
+ if kw.arg == "entry" and isinstance(kw.value, ast.Call):
865
+ entry_call = kw.value
866
+ if (
867
+ isinstance(entry_call.func, ast.Name)
868
+ and entry_call.func.id == "TaskAppEntry"
869
+ ):
870
+ for entry_kw in entry_call.keywords:
871
+ if entry_kw.arg == "modal" and isinstance(entry_kw.value, ast.Call):
872
+ modal_call = entry_kw.value
873
+ if (
874
+ isinstance(modal_call.func, ast.Name)
875
+ and modal_call.func.id == "ModalDeploymentConfig"
876
+ ):
877
+ return True
878
+ except Exception:
879
+ pass
880
+ return False
881
+
882
+
883
+ def _extract_modal_config_from_file(path: Path) -> ModalDeploymentConfigType | None:
884
+ """Extract ModalDeploymentConfig from a file by parsing the AST."""
885
+ try:
886
+ source = path.read_text(encoding="utf-8")
887
+ tree = ast.parse(source, filename=str(path))
888
+
889
+ # Look for ModalDeploymentConfig in register_task_app calls
890
+ for node in ast.walk(tree):
891
+ if isinstance(node, ast.Call) and _is_register_task_app_call(node):
892
+ # Check if the entry has modal=ModalDeploymentConfig(...)
893
+ for kw in node.keywords:
894
+ if kw.arg == "entry" and isinstance(kw.value, ast.Call):
895
+ entry_call = kw.value
896
+ if (
897
+ isinstance(entry_call.func, ast.Name)
898
+ and entry_call.func.id == "TaskAppEntry"
899
+ ):
900
+ for entry_kw in entry_call.keywords:
901
+ if entry_kw.arg == "modal" and isinstance(entry_kw.value, ast.Call):
902
+ modal_call = entry_kw.value
903
+ if (
904
+ isinstance(modal_call.func, ast.Name)
905
+ and modal_call.func.id == "ModalDeploymentConfig"
906
+ ):
907
+ # Extract the arguments to ModalDeploymentConfig
908
+ return _build_modal_config_from_ast(modal_call)
909
+ except Exception:
910
+ pass
911
+ return None
912
+
913
+
914
+ def _build_modal_config_from_ast(modal_call: ast.Call) -> ModalDeploymentConfigType | None:
915
+ """Build a ModalDeploymentConfig from an AST Call node."""
916
+ try:
917
+ # Extract keyword arguments
918
+ kwargs = {}
919
+ for kw in modal_call.keywords:
920
+ if kw.arg and isinstance(kw.value, ast.Constant):
921
+ kwargs[kw.arg] = kw.value.value
922
+ elif kw.arg == "pip_packages" and isinstance(kw.value, ast.List | ast.Tuple):
923
+ # Handle pip_packages list/tuple
924
+ packages: list[str] = []
925
+ value_node = kw.value
926
+ if isinstance(value_node, ast.List | ast.Tuple):
927
+ for elt in value_node.elts:
928
+ if isinstance(elt, ast.Constant):
929
+ packages.append(str(elt.value))
930
+ kwargs[kw.arg] = tuple(packages)
931
+ elif kw.arg == "extra_local_dirs" and isinstance(kw.value, ast.List | ast.Tuple):
932
+ # Handle extra_local_dirs list/tuple of tuples
933
+ dirs = []
934
+ value_node = kw.value
935
+ if isinstance(value_node, ast.List | ast.Tuple):
936
+ for elt in value_node.elts:
937
+ if isinstance(elt, ast.List | ast.Tuple) and len(elt.elts) == 2:
938
+ src = elt.elts[0].value if isinstance(elt.elts[0], ast.Constant) else None
939
+ dst = elt.elts[1].value if isinstance(elt.elts[1], ast.Constant) else None
940
+ if src and dst:
941
+ dirs.append((src, dst))
942
+ kwargs[kw.arg] = tuple(dirs)
943
+ elif kw.arg == "secret_names" and isinstance(kw.value, ast.List | ast.Tuple):
944
+ # Handle secret_names list/tuple
945
+ secrets = []
946
+ value_node = kw.value
947
+ if isinstance(value_node, ast.List | ast.Tuple):
948
+ for elt in value_node.elts:
949
+ if isinstance(elt, ast.Constant):
950
+ secrets.append(elt.value)
951
+ kwargs[kw.arg] = tuple(secrets)
952
+ elif kw.arg == "volume_mounts" and isinstance(kw.value, ast.List | ast.Tuple):
953
+ # Handle volume_mounts list/tuple of tuples
954
+ mounts = []
955
+ value_node = kw.value
956
+ if isinstance(value_node, ast.List | ast.Tuple):
957
+ for elt in value_node.elts:
958
+ if isinstance(elt, ast.List | ast.Tuple) and len(elt.elts) == 2:
959
+ name = elt.elts[0].value if isinstance(elt.elts[0], ast.Constant) else None
960
+ mount = elt.elts[1].value if isinstance(elt.elts[1], ast.Constant) else None
961
+ if name and mount:
962
+ mounts.append((name, mount))
963
+ kwargs[kw.arg] = tuple(mounts)
964
+
965
+ return ModalDeploymentConfig(**kwargs)
966
+ except Exception:
967
+ return None
968
+
969
+
970
+ def _choice_has_local_support(choice: AppChoice) -> bool:
971
+ if choice.modal_script:
972
+ return False
973
+ try:
974
+ choice.ensure_entry()
975
+ except click.ClickException:
976
+ return False
977
+ return True
978
+
979
+
980
+ def _format_choice(choice: AppChoice, index: int | None = None) -> str:
981
+ prefix = f"[{index}] " if index is not None else ""
982
+ # Get file modification timestamp
983
+ try:
984
+ from datetime import datetime
985
+
986
+ mtime = choice.path.stat().st_mtime
987
+ modified_str = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
988
+ details = f"Modified: {modified_str}"
989
+ except Exception:
990
+ # Fallback if timestamp unavailable
991
+ details = choice.description or "No timestamp available"
992
+ # Format: single line with timestamp
993
+ main_line = f"{prefix}{choice.app_id} ({choice.source}) – {details}"
994
+ return main_line
995
+
996
+
997
+ def _prompt_user_for_choice(choices: list[AppChoice]) -> AppChoice:
998
+ click.echo("Select a task app:")
999
+ for idx, choice in enumerate(choices, start=1):
1000
+ click.echo(_format_choice(choice, idx))
1001
+ try:
1002
+ response = click.prompt("Enter choice", default="1", type=str).strip() or "1"
1003
+ except (Abort, EOFError, KeyboardInterrupt) as exc:
1004
+ raise click.ClickException("Task app selection cancelled by user") from exc
1005
+ if not response.isdigit():
1006
+ raise click.ClickException("Selection must be a number")
1007
+ index = int(response)
1008
+ if not 1 <= index <= len(choices):
1009
+ raise click.ClickException("Selection out of range")
1010
+ return choices[index - 1]
1011
+
1012
+
1013
+ def _select_app_choice(app_id: str | None, purpose: str) -> AppChoice:
1014
+ choices = _collect_task_app_choices()
1015
+ if purpose in {"serve", "eval"}:
1016
+ filtered = [c for c in choices if not c.modal_script]
1017
+ elif purpose in {"deploy", "modal-serve"}:
1018
+ filtered = []
1019
+ for choice in choices:
1020
+ if choice.modal_script or _choice_has_modal_support(choice):
1021
+ filtered.append(choice)
1022
+ else:
1023
+ filtered = choices
1024
+
1025
+ filtered.sort(key=_app_choice_sort_key)
1026
+
1027
+ if not filtered:
1028
+ raise click.ClickException("No task apps discovered for this command.")
1029
+
1030
+ if app_id:
1031
+ matches = [c for c in filtered if _choice_matches_identifier(c, app_id)]
1032
+ if not matches:
1033
+ available = ", ".join(sorted({c.app_id for c in filtered}))
1034
+ raise click.ClickException(f"Task app '{app_id}' not found. Available: {available}")
1035
+ exact_matches = [c for c in matches if c.app_id == app_id]
1036
+ if len(exact_matches) == 1:
1037
+ return exact_matches[0]
1038
+ if len(matches) == 1:
1039
+ return matches[0]
1040
+ # Prefer entries with modal support when required
1041
+ if purpose in {"deploy", "modal-serve"}:
1042
+ modal_matches = [c for c in matches if _choice_has_modal_support(c)]
1043
+ if len(modal_matches) == 1:
1044
+ return modal_matches[0]
1045
+ if modal_matches:
1046
+ matches = modal_matches
1047
+ return _prompt_user_for_choice(matches)
1048
+
1049
+ if len(filtered) == 1:
1050
+ choice = filtered[0]
1051
+ click.echo(_format_choice(choice))
1052
+ return choice
1053
+
1054
+ return _prompt_user_for_choice(filtered)
1055
+
1056
+
1057
+ def _import_task_app_module(
1058
+ resolved: Path,
1059
+ module_name: str,
1060
+ *,
1061
+ namespace_root: Path | None,
1062
+ sys_path_roots: Sequence[Path],
1063
+ ensure_namespace: bool = True,
1064
+ ) -> types.ModuleType:
1065
+ spec = importlib.util.spec_from_file_location(module_name, str(resolved))
1066
+ if spec is None or spec.loader is None:
1067
+ raise click.ClickException(f"Unable to load Python module from {resolved}")
1068
+
1069
+ module = importlib.util.module_from_spec(spec)
1070
+ sys.modules[module_name] = module
1071
+
1072
+ with _temporary_sys_path(sys_path_roots):
1073
+ if ensure_namespace and namespace_root is not None and "." in module_name:
1074
+ _ensure_parent_namespace(module_name, namespace_root)
1075
+
1076
+ # Clear registry before importing to avoid duplicate registration errors
1077
+ registry.clear()
1078
+
1079
+ try:
1080
+ spec.loader.exec_module(module)
1081
+ except Exception:
1082
+ # Remove partially-imported module to avoid reuse
1083
+ sys.modules.pop(module_name, None)
1084
+ raise
1085
+
1086
+ return module
1087
+
1088
+
1089
+ @contextlib.contextmanager
1090
+ def _safe_import_context() -> Iterator[None]:
1091
+ """Guard module imports against argparse/uvicorn side effects."""
1092
+
1093
+ original_argv = sys.argv[:]
1094
+ sys.argv = [original_argv[0]] if original_argv else ["python"]
1095
+
1096
+ parser_cls = argparse.ArgumentParser
1097
+ old_parse_args = parser_cls.parse_args
1098
+
1099
+ def _parse_noargs(self, args=None, namespace=None): # type: ignore[override]
1100
+ if args is None:
1101
+ args = []
1102
+ if namespace is None:
1103
+ namespace = argparse.Namespace()
1104
+ try:
1105
+ return old_parse_args(self, args, namespace)
1106
+ except SystemExit:
1107
+ return namespace
1108
+
1109
+ parser_cls.parse_args = _parse_noargs # type: ignore[assignment]
1110
+
1111
+ uvicorn_run = None
1112
+ run_task_app_orig = None
1113
+ try:
1114
+ import uvicorn # type: ignore
1115
+
1116
+ uvicorn_run = uvicorn.run
1117
+ uvicorn.run = lambda *args, **kwargs: None # type: ignore[assignment]
1118
+ except Exception:
1119
+ uvicorn_run = None
1120
+
1121
+ try:
1122
+ _task_server_patch = cast( # type: ignore[redundant-cast]
1123
+ Any, importlib.import_module("synth_ai.sdk.task.server")
1124
+ )
1125
+ run_task_app_orig = cast(Callable[..., Any], _task_server_patch.run_task_app)
1126
+ _task_server_patch.run_task_app = ( # type: ignore[assignment]
1127
+ lambda *args, **kwargs: None
1128
+ )
1129
+ except Exception:
1130
+ run_task_app_orig = None
1131
+
1132
+ try:
1133
+ yield
1134
+ finally:
1135
+ sys.argv = original_argv
1136
+ parser_cls.parse_args = old_parse_args # type: ignore[assignment]
1137
+ if uvicorn_run is not None:
1138
+ try:
1139
+ import uvicorn # type: ignore
1140
+
1141
+ uvicorn.run = uvicorn_run # type: ignore[assignment]
1142
+ except Exception:
1143
+ pass
1144
+ if run_task_app_orig is not None:
1145
+ try:
1146
+ _task_server_patch = cast(
1147
+ Any, importlib.import_module("synth_ai.sdk.task.server")
1148
+ )
1149
+ _task_server_patch.run_task_app = run_task_app_orig # type: ignore[assignment]
1150
+ except Exception:
1151
+ pass
1152
+
1153
+
1154
+ def _load_entry_from_path(
1155
+ path: Path, app_id: str, module_search_roots: Sequence[Path] | None = None
1156
+ ) -> TaskAppEntryType:
1157
+ resolved = path.resolve()
1158
+ search_roots: list[Path] = []
1159
+ seen_roots: set[Path] = set()
1160
+
1161
+ def _append_root(candidate: Path) -> None:
1162
+ try:
1163
+ resolved_root = candidate.resolve()
1164
+ except Exception:
1165
+ return
1166
+ if resolved_root in seen_roots:
1167
+ return
1168
+ seen_roots.add(resolved_root)
1169
+ search_roots.append(resolved_root)
1170
+
1171
+ for root in module_search_roots or []:
1172
+ _append_root(root)
1173
+ _append_root(resolved.parent)
1174
+ _append_root(REPO_ROOT)
1175
+
1176
+ last_error: Exception | None = None
1177
+ module: types.ModuleType | None = None
1178
+
1179
+ for module_name, namespace_root in _possible_module_names(resolved, search_roots):
1180
+ try:
1181
+ with _safe_import_context():
1182
+ module = _import_task_app_module(
1183
+ resolved,
1184
+ module_name,
1185
+ namespace_root=namespace_root,
1186
+ sys_path_roots=search_roots,
1187
+ ensure_namespace=True,
1188
+ )
1189
+ break
1190
+ except Exception as exc: # pragma: no cover - best-effort fallbacks
1191
+ last_error = exc
1192
+ continue
1193
+
1194
+ if module is None:
1195
+ hashed_name = f"_synth_task_app_{hashlib.md5(str(resolved).encode(), usedforsecurity=False).hexdigest()}"
1196
+ try:
1197
+ with _safe_import_context():
1198
+ module = _import_task_app_module(
1199
+ resolved,
1200
+ hashed_name,
1201
+ namespace_root=None,
1202
+ sys_path_roots=search_roots,
1203
+ ensure_namespace=False,
1204
+ )
1205
+ except Exception as exc: # pragma: no cover - propagate meaningful error
1206
+ detail = last_error or exc
1207
+ raise click.ClickException(f"Failed to import {resolved}: {detail}") from detail
1208
+
1209
+ config_obj: TaskAppConfigType | None = None
1210
+ factory_callable: Callable[[], TaskAppConfigType] | None = None
1211
+
1212
+ for attr_name in dir(module):
1213
+ try:
1214
+ attr = getattr(module, attr_name)
1215
+ except Exception:
1216
+ continue
1217
+ if isinstance(attr, TaskAppConfig) and attr.app_id == app_id:
1218
+ config_obj = attr
1219
+
1220
+ def _return_config(cfg: TaskAppConfigType = attr) -> TaskAppConfigType:
1221
+ return cfg
1222
+
1223
+ factory_callable = _return_config
1224
+ break
1225
+
1226
+ if factory_callable is None:
1227
+ for attr_name in dir(module):
1228
+ if attr_name.startswith("_"):
1229
+ continue
1230
+ try:
1231
+ attr = getattr(module, attr_name)
1232
+ except Exception:
1233
+ continue
1234
+ if not callable(attr):
1235
+ continue
1236
+ try:
1237
+ sig = inspect.signature(attr)
1238
+ except (TypeError, ValueError):
1239
+ continue
1240
+ has_required = False
1241
+ for param in sig.parameters.values():
1242
+ if (
1243
+ param.kind
1244
+ in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD)
1245
+ and param.default is inspect._empty
1246
+ ):
1247
+ has_required = True
1248
+ break
1249
+ if has_required:
1250
+ continue
1251
+ try:
1252
+ with _safe_import_context():
1253
+ result = attr()
1254
+ except SystemExit:
1255
+ continue
1256
+ except Exception:
1257
+ continue
1258
+ if isinstance(result, TaskAppConfig) and result.app_id == app_id:
1259
+ # Bind attr to a local and close over it without exposing parameters
1260
+ bound_func: Callable[[], TaskAppConfig] = cast(Callable[[], TaskAppConfig], attr) # type: ignore[assignment]
1261
+
1262
+ def _factory_noargs(
1263
+ func: Callable[[], TaskAppConfigType] = bound_func,
1264
+ ) -> TaskAppConfigType:
1265
+ return func()
1266
+
1267
+ factory_callable = _factory_noargs
1268
+ config_obj = result
1269
+ break
1270
+
1271
+ # If no TaskAppConfig found directly, check if it was registered via register_task_app
1272
+ if factory_callable is None or config_obj is None:
1273
+ try:
1274
+ # Check if the app was registered in the registry
1275
+ entry = registry.get(app_id)
1276
+ return entry
1277
+ except KeyError as exc:
1278
+ raise click.ClickException(
1279
+ f"Could not locate TaskAppConfig for '{app_id}' in {resolved}."
1280
+ ) from exc
1281
+
1282
+ modal_cfg: ModalDeploymentConfigType | None = None
1283
+ for attr_name in dir(module):
1284
+ try:
1285
+ attr = getattr(module, attr_name)
1286
+ except Exception:
1287
+ continue
1288
+ if isinstance(attr, ModalDeploymentConfig):
1289
+ modal_cfg = attr
1290
+ break
1291
+
1292
+ # If no ModalDeploymentConfig found, try to detect it via AST parsing
1293
+ if modal_cfg is None:
1294
+ modal_cfg = _extract_modal_config_from_file(resolved)
1295
+
1296
+ description = inspect.getdoc(module) or f"Discovered task app in {resolved.name}"
1297
+ env_files: Iterable[str] = getattr(module, "ENV_FILES", ()) # type: ignore[arg-type]
1298
+
1299
+ entry = TaskAppEntry(
1300
+ app_id=app_id,
1301
+ description=description,
1302
+ config_factory=factory_callable,
1303
+ aliases=(),
1304
+ env_files=tuple(str(Path(p)) for p in env_files if p),
1305
+ modal=modal_cfg,
1306
+ )
1307
+ return entry
1308
+
1309
+
1310
+ def _resolve_env_paths_for_script(script_path: Path, explicit: Sequence[str]) -> list[Path]:
1311
+ if explicit:
1312
+ resolved: list[Path] = []
1313
+ for candidate in explicit:
1314
+ p = Path(candidate).expanduser()
1315
+ if not p.exists():
1316
+ raise click.ClickException(f"Env file not found: {p}")
1317
+ resolved.append(p)
1318
+ return resolved
1319
+
1320
+ # Always prompt for env file selection instead of auto-loading defaults
1321
+ script_dir = script_path.parent.resolve()
1322
+ cwd = Path.cwd()
1323
+
1324
+ # Look for env files in current working directory first, then repo root
1325
+ env_candidates = []
1326
+
1327
+ # Add CWD env files first (prioritized)
1328
+ cwd_env_files = sorted(cwd.glob("**/*.env"))
1329
+ env_candidates.extend(cwd_env_files)
1330
+
1331
+ # Add repo root env files
1332
+ repo_env_files = sorted(REPO_ROOT.glob("**/*.env"))
1333
+ # Avoid duplicates
1334
+ for repo_file in repo_env_files:
1335
+ if repo_file not in env_candidates:
1336
+ env_candidates.append(repo_file)
1337
+
1338
+ if not env_candidates:
1339
+ created = _interactive_create_env(script_dir)
1340
+ if created is None:
1341
+ raise click.ClickException("Env file required (--env-file) for this task app")
1342
+ return [created]
1343
+
1344
+ click.echo("Select env file to load:")
1345
+ for idx, path in enumerate(env_candidates, start=1):
1346
+ click.echo(f" {idx}) {path.resolve()}")
1347
+ choice = click.prompt("Enter choice", type=click.IntRange(1, len(env_candidates)), default=1)
1348
+ return [env_candidates[choice - 1]]
1349
+
1350
+
1351
+ def _path_is_within(child: Path, parent: Path) -> bool:
1352
+ try:
1353
+ child.resolve().relative_to(parent.resolve())
1354
+ return True
1355
+ except Exception:
1356
+ return False
1357
+
1358
+
1359
+ @functools.lru_cache(maxsize=16)
1360
+ def _is_modal_shim(path_str: str) -> bool:
1361
+ """Return True if the candidate CLI path refers to the synth-ai shim."""
1362
+
1363
+ path = Path(path_str)
1364
+ try:
1365
+ resolved = path.resolve(strict=True)
1366
+ except Exception:
1367
+ resolved = path
1368
+
1369
+ if not resolved.exists() or resolved.is_dir():
1370
+ return False
1371
+
1372
+ snippet = ""
1373
+ try:
1374
+ snippet = resolved.read_bytes()[:4096].decode("utf-8", errors="ignore")
1375
+ except Exception:
1376
+ snippet = ""
1377
+
1378
+ shim_markers = (
1379
+ "synth_ai.cli._modal_wrapper",
1380
+ "from modal.__main__ import main",
1381
+ "import modal.__main__",
1382
+ "run_module('modal.__main__'",
1383
+ )
1384
+ if snippet and any(marker in snippet for marker in shim_markers):
1385
+ return True
1386
+
1387
+ try:
1388
+ size = resolved.stat().st_size
1389
+ except Exception:
1390
+ size = None
1391
+
1392
+ if (
1393
+ size is not None
1394
+ and size < 2048
1395
+ and "python" in (snippet.splitlines() or [""])[0]
1396
+ and (
1397
+ "modal.__main__" in snippet
1398
+ or "modal.__main__" in snippet.replace(" ", "")
1399
+ )
1400
+ ):
1401
+ return True
1402
+
1403
+ virtual_env = os.environ.get("VIRTUAL_ENV")
1404
+ if virtual_env and _path_is_within(resolved, Path(virtual_env)):
1405
+ return True
1406
+
1407
+ if _path_is_within(resolved, REPO_ROOT):
1408
+ return True
1409
+
1410
+ uv_tools_dir = Path.home() / ".local" / "share" / "uv" / "tools"
1411
+ return uv_tools_dir.exists() and _path_is_within(resolved, uv_tools_dir)
1412
+
1413
+
1414
+ def _find_modal_executable(modal_cli: str) -> tuple[str | None, str | None]:
1415
+ """Return the first non-shim executable and the first shim discovered on PATH."""
1416
+
1417
+ if not modal_cli:
1418
+ modal_cli = "modal"
1419
+
1420
+ candidate_path = Path(modal_cli).expanduser()
1421
+ if candidate_path.is_absolute() or len(candidate_path.parts) > 1:
1422
+ resolved_candidate = candidate_path
1423
+ if not resolved_candidate.is_absolute():
1424
+ resolved_candidate = (Path.cwd() / resolved_candidate).resolve()
1425
+ else:
1426
+ resolved_candidate = resolved_candidate.resolve()
1427
+ if not resolved_candidate.exists():
1428
+ raise click.ClickException(f"--modal-cli path does not exist: {resolved_candidate}")
1429
+ if not os.access(resolved_candidate, os.X_OK):
1430
+ raise click.ClickException(f"--modal-cli is not executable: {resolved_candidate}")
1431
+ return str(resolved_candidate), None
1432
+
1433
+ path_env = os.environ.get("PATH", "")
1434
+ if not path_env:
1435
+ return None, None
1436
+
1437
+ seen_dirs: set[str] = set()
1438
+ seen_candidates: set[str] = set()
1439
+ shim_path: str | None = None
1440
+
1441
+ for raw_entry in path_env.split(os.pathsep):
1442
+ if not raw_entry:
1443
+ continue
1444
+ try:
1445
+ resolved_entry = str(Path(raw_entry).resolve())
1446
+ except Exception:
1447
+ resolved_entry = os.path.normpath(raw_entry)
1448
+ if resolved_entry in seen_dirs:
1449
+ continue
1450
+ seen_dirs.add(resolved_entry)
1451
+
1452
+ candidate = shutil.which(modal_cli, path=raw_entry)
1453
+ if candidate is None:
1454
+ continue
1455
+ if candidate in seen_candidates:
1456
+ continue
1457
+ seen_candidates.add(candidate)
1458
+
1459
+ if _is_modal_shim(candidate):
1460
+ if shim_path is None:
1461
+ shim_path = candidate
1462
+ continue
1463
+ return candidate, shim_path
1464
+
1465
+ return None, shim_path
1466
+
1467
+
1468
+ def _modal_command_prefix(modal_cli: str) -> list[str]:
1469
+ """Resolve a command prefix for invoking the Modal CLI within the active environment."""
1470
+
1471
+ force_wrapper_env = os.environ.get("SYNTH_FORCE_MODAL_WRAPPER", "").strip().lower()
1472
+ if force_wrapper_env in {"1", "true", "yes"}:
1473
+ click.secho(
1474
+ "[modal-prefix] SYNTH_FORCE_MODAL_WRAPPER=1 -> using in-process wrapper",
1475
+ fg="yellow",
1476
+ )
1477
+ return [sys.executable, "-m", "synth_ai.cli._modal_wrapper"]
1478
+
1479
+ lookup = modal_cli or "modal"
1480
+ spec = importlib.util.find_spec("modal") if lookup == "modal" else None
1481
+
1482
+ preferred, shim_candidate = _find_modal_executable(lookup)
1483
+ if preferred is not None:
1484
+ detail = f"[modal-prefix] modal_cli={lookup} selected={preferred}"
1485
+ if lookup == "modal":
1486
+ detail += f" spec={'yes' if spec else 'no'}"
1487
+ click.secho(detail, fg="cyan")
1488
+ return [preferred]
1489
+
1490
+ if lookup != "modal":
1491
+ raise click.ClickException(f"Modal CLI not found (looked for '{lookup}')")
1492
+
1493
+ if spec is not None:
1494
+ warning = "[modal-prefix] Using synth-ai modal shim; pass --modal-cli /path/to/modal to override."
1495
+ if shim_candidate is not None:
1496
+ warning = (
1497
+ f"[modal-prefix] Using synth-ai modal shim at {shim_candidate}; "
1498
+ "pass --modal-cli /path/to/modal to override."
1499
+ )
1500
+ click.secho(warning, fg="yellow")
1501
+ click.secho(
1502
+ "[modal-prefix] modal_cli=modal selected=module-wrapper spec=yes",
1503
+ fg="yellow",
1504
+ )
1505
+ return [sys.executable, "-m", "synth_ai.cli._modal_wrapper"]
1506
+
1507
+ if shim_candidate is not None:
1508
+ raise click.ClickException(
1509
+ "Modal CLI resolution found the synth-ai shim but the 'modal' package "
1510
+ "is not importable in this environment. Install the official Modal CLI "
1511
+ "or pass --modal-cli with its path."
1512
+ )
1513
+
1514
+ raise click.ClickException(
1515
+ "Modal CLI not found. Install the 'modal' package in this environment or pass "
1516
+ "--modal-cli with an explicit path."
1517
+ )
1518
+
1519
+
1520
+ def _build_modal_app_wrapper(original_script: Path) -> tuple[Path, Path]:
1521
+ source_dir = original_script.parent.resolve()
1522
+ repo_root = REPO_ROOT
1523
+ temp_root = Path(tempfile.mkdtemp(prefix="synth_modal_app_"))
1524
+
1525
+ wrapper_source = textwrap.dedent(
1526
+ f"""
1527
+ from importlib import util as _util
1528
+ from pathlib import Path as _Path
1529
+ import sys as _sys
1530
+
1531
+ _source_dir = _Path({str(source_dir)!r}).resolve()
1532
+ _module_path = _source_dir / {original_script.name!r}
1533
+ _package_name = _source_dir.name
1534
+ _repo_root = _Path({str(repo_root)!r}).resolve()
1535
+ _synth_dir = _repo_root / "synth_ai"
1536
+
1537
+ for _path in (str(_source_dir), str(_source_dir.parent), str(_repo_root)):
1538
+ if _path not in _sys.path:
1539
+ _sys.path.insert(0, _path)
1540
+
1541
+ _spec = _util.spec_from_file_location("_synth_modal_target", str(_module_path))
1542
+ if _spec is None or _spec.loader is None:
1543
+ raise SystemExit("Unable to load modal task app from {original_script}")
1544
+ _module = _util.module_from_spec(_spec)
1545
+ _sys.modules.setdefault("_synth_modal_target", _module)
1546
+ _spec.loader.exec_module(_module)
1547
+
1548
+ try:
1549
+ from modal import App as _ModalApp
1550
+ from modal import Image as _ModalImage
1551
+ except Exception:
1552
+ _ModalApp = None # type: ignore[assignment]
1553
+ _ModalImage = None # type: ignore[assignment]
1554
+
1555
+ def _apply_local_mounts(image):
1556
+ if _ModalImage is None or not isinstance(image, _ModalImage):
1557
+ return image
1558
+ mounts = [
1559
+ (str(_source_dir), f"/root/{{_package_name}}"),
1560
+ (str(_synth_dir), "/root/synth_ai"),
1561
+ ]
1562
+ for local_path, remote_path in mounts:
1563
+ try:
1564
+ image = image.add_local_dir(local_path, remote_path=remote_path)
1565
+ except Exception:
1566
+ pass
1567
+ return image
1568
+
1569
+ if hasattr(_module, "image"):
1570
+ _module.image = _apply_local_mounts(getattr(_module, "image"))
1571
+
1572
+ _candidate = getattr(_module, "app", None)
1573
+ if _ModalApp is None or not isinstance(_candidate, _ModalApp):
1574
+ candidate_modal_app = getattr(_module, "modal_app", None)
1575
+ if _ModalApp is not None and isinstance(candidate_modal_app, _ModalApp):
1576
+ _candidate = candidate_modal_app
1577
+ setattr(_module, "app", _candidate)
1578
+
1579
+ if _ModalApp is not None and not isinstance(_candidate, _ModalApp):
1580
+ raise SystemExit(
1581
+ "Modal task app must expose an 'app = modal.App(...)' (or modal_app) attribute."
1582
+ )
1583
+
1584
+ for remote_path in ("/root/synth_ai", f"/root/{{_package_name}}"):
1585
+ if remote_path not in _sys.path:
1586
+ _sys.path.insert(0, remote_path)
1587
+
1588
+ globals().update({{k: v for k, v in vars(_module).items() if not k.startswith("__")}})
1589
+ app = getattr(_module, "app")
1590
+ """
1591
+ ).strip()
1592
+
1593
+ wrapper_path = temp_root / "__modal_wrapper__.py"
1594
+ wrapper_path.write_text(wrapper_source + "\n", encoding="utf-8")
1595
+ return wrapper_path, temp_root
1596
+
1597
+
1598
+
1599
+ def _run_modal_script(
1600
+ script_path: Path,
1601
+ modal_cli: str,
1602
+ command: str,
1603
+ env_paths: Sequence[Path],
1604
+ *,
1605
+ modal_name: str | None = None,
1606
+ dry_run: bool = False,
1607
+ ) -> None:
1608
+ env_paths_list = [Path(p).resolve() for p in env_paths]
1609
+ path_strings = [str(p) for p in env_paths_list]
1610
+ _load_env_files_into_process(path_strings)
1611
+ _ensure_env_values(env_paths_list, script_path.parent)
1612
+ _load_env_values(env_paths_list)
1613
+ # Ensure ENVIRONMENT_API_KEY is uploaded to backend for this org (matches registry path behavior)
1614
+ try:
1615
+ _preflight_env_key(env_paths_list, crash_on_failure=True)
1616
+ except Exception as _pf_err:
1617
+ raise click.ClickException(str(_pf_err)) from _pf_err
1618
+
1619
+ proc_env = os.environ.copy()
1620
+ pythonpath_entries: list[str] = []
1621
+ script_dir = script_path.parent.resolve()
1622
+ pythonpath_entries.append(str(script_dir))
1623
+ if (script_dir / "__init__.py").exists():
1624
+ # Script lives inside a package; ensure the parent package directory is importable.
1625
+ pythonpath_entries.append(str(script_dir.parent.resolve()))
1626
+ pythonpath_entries.append(str(REPO_ROOT))
1627
+ existing_pp = proc_env.get("PYTHONPATH")
1628
+ if existing_pp:
1629
+ pythonpath_entries.append(existing_pp)
1630
+ unique_paths = list(dict.fromkeys(pythonpath_entries))
1631
+ proc_env["PYTHONPATH"] = os.pathsep.join(unique_paths)
1632
+
1633
+ wrapper_info: tuple[Path, Path] | None = None
1634
+ target_script = script_path
1635
+ if command in {"serve", "deploy"}:
1636
+ wrapper_path, temp_root = _build_modal_app_wrapper(script_path)
1637
+ wrapper_info = (wrapper_path, temp_root)
1638
+ target_script = wrapper_path
1639
+
1640
+ # Ensure the wrapper has access to the Synth AI source for intra-repo imports
1641
+ if "PYTHONPATH" in proc_env:
1642
+ proc_env["PYTHONPATH"] = os.pathsep.join(
1643
+ [str(REPO_ROOT)] + proc_env["PYTHONPATH"].split(os.pathsep)
1644
+ )
1645
+ else:
1646
+ proc_env["PYTHONPATH"] = str(REPO_ROOT)
1647
+
1648
+ cmd = [*_modal_command_prefix(modal_cli), command, str(target_script)]
1649
+ if modal_name and command == "deploy":
1650
+ cmd.extend(["--name", modal_name])
1651
+ if dry_run:
1652
+ click.echo(
1653
+ "Dry run: " + " ".join(shlex.quote(component) for component in cmd),
1654
+ err=False,
1655
+ )
1656
+ return
1657
+ click.secho(
1658
+ "[modal-exec] " + " ".join(shlex.quote(component) for component in cmd),
1659
+ fg="cyan",
1660
+ )
1661
+ try:
1662
+ # Stream output live for better diagnostics
1663
+ proc = subprocess.Popen(
1664
+ cmd,
1665
+ stdout=subprocess.PIPE,
1666
+ stderr=subprocess.STDOUT,
1667
+ text=True,
1668
+ bufsize=1,
1669
+ env=proc_env,
1670
+ )
1671
+ task_app_url = None
1672
+ assert proc.stdout is not None
1673
+ for line in proc.stdout:
1674
+ click.echo(line, nl=False)
1675
+ if task_app_url is None and ("modal.run" in line and "=>" in line):
1676
+ parts = line.split("=>")
1677
+ if len(parts) >= 2:
1678
+ task_app_url = parts[-1].strip()
1679
+ if task_app_url and env_paths_list:
1680
+ env_file = env_paths_list[0]
1681
+ _save_to_env_file(env_file, "TASK_APP_BASE_URL", task_app_url)
1682
+ click.echo(f"\n✓ Task app URL: {task_app_url}\n")
1683
+ rc = proc.wait()
1684
+ if rc != 0:
1685
+ raise subprocess.CalledProcessError(rc, cmd)
1686
+ except subprocess.CalledProcessError as exc:
1687
+ raise click.ClickException(
1688
+ f"modal {command} failed with exit code {exc.returncode}"
1689
+ ) from exc
1690
+ finally:
1691
+ if wrapper_info is not None:
1692
+ wrapper_path, temp_root = wrapper_info
1693
+ with contextlib.suppress(Exception):
1694
+ wrapper_path.unlink(missing_ok=True)
1695
+ shutil.rmtree(temp_root, ignore_errors=True)
1696
+
1697
+
1698
+ def _preflight_env_key(env_paths: Sequence[Path] | None = None, *, crash_on_failure: bool = False) -> None:
1699
+ try:
1700
+ raw_backend = (
1701
+ os.environ.get("BACKEND_BASE_URL")
1702
+ or os.environ.get("SYNTH_BASE_URL")
1703
+ or f"{PROD_BASE_URL_DEFAULT}/api"
1704
+ )
1705
+ backend_base = raw_backend.rstrip("/")
1706
+ if not backend_base.endswith("/api"):
1707
+ backend_base = backend_base + "/api"
1708
+ synth_key = os.environ.get("SYNTH_API_KEY") or ""
1709
+ env_api_key = (
1710
+ os.environ.get("ENVIRONMENT_API_KEY") or os.environ.get("DEV_ENVIRONMENT_API_KEY") or ""
1711
+ ).strip()
1712
+
1713
+ def _preview(value: str) -> str:
1714
+ if len(value) <= 10:
1715
+ return value
1716
+ return f"{value[:6]}...{value[-4:]}"
1717
+
1718
+ minted = False
1719
+ if not env_api_key:
1720
+ secrets_module = _maybe_import("synth_ai.sdk.learning.rl.secrets")
1721
+ try:
1722
+ if secrets_module is None:
1723
+ raise RuntimeError("secrets module unavailable")
1724
+ mint_env_key = secrets_module.mint_environment_api_key
1725
+ env_api_key = mint_env_key()
1726
+ os.environ["ENVIRONMENT_API_KEY"] = env_api_key
1727
+ os.environ.setdefault("DEV_ENVIRONMENT_API_KEY", env_api_key)
1728
+ minted = True
1729
+ click.echo(
1730
+ f"[preflight] minted ENVIRONMENT_API_KEY ({_preview(env_api_key)})"
1731
+ )
1732
+ except Exception as mint_err:
1733
+ if crash_on_failure:
1734
+ raise click.ClickException(
1735
+ f"[CRITICAL] Failed to mint ENVIRONMENT_API_KEY: {mint_err}"
1736
+ ) from mint_err
1737
+ click.echo(
1738
+ f"[WARN] Failed to mint ENVIRONMENT_API_KEY automatically ({mint_err}); proceeding without upload"
1739
+ )
1740
+
1741
+ if env_api_key and not os.environ.get("ENVIRONMENT_API_KEY"):
1742
+ os.environ["ENVIRONMENT_API_KEY"] = env_api_key
1743
+ if env_api_key and not os.environ.get("DEV_ENVIRONMENT_API_KEY"):
1744
+ os.environ["DEV_ENVIRONMENT_API_KEY"] = env_api_key
1745
+
1746
+ if minted:
1747
+ _persist_env_api_key(env_api_key, env_paths)
1748
+
1749
+ if synth_key and env_api_key:
1750
+ import base64
1751
+
1752
+ import httpx
1753
+
1754
+ click.echo(f"[preflight] backend={backend_base}")
1755
+ with httpx.Client(timeout=15.0, headers={"Authorization": f"Bearer {synth_key}"}) as c:
1756
+ click.echo("[preflight] fetching public key…")
1757
+ rpk = c.get(f"{backend_base.rstrip('/')}/v1/crypto/public-key")
1758
+ pk = (rpk.json() or {}).get("public_key") if rpk.status_code == 200 else None
1759
+ if pk:
1760
+ try:
1761
+ from nacl.public import PublicKey, SealedBox
1762
+
1763
+ # Decode public key and build sealed box
1764
+ pk_bytes = base64.b64decode(pk, validate=True)
1765
+ pub = PublicKey(pk_bytes)
1766
+ sb = SealedBox(pub)
1767
+
1768
+ # Encrypt plaintext key
1769
+ ct_b64 = base64.b64encode(sb.encrypt(env_api_key.encode("utf-8"))).decode()
1770
+ payload = {"name": "ENVIRONMENT_API_KEY", "ciphertext_b64": ct_b64}
1771
+
1772
+ # Emit diagnostic logging (safe previews + hashes only)
1773
+ try:
1774
+ import hashlib as _hash
1775
+
1776
+ # Backend URL context
1777
+ click.echo(f"[preflight] posting to {backend_base.rstrip('/')}/v1/env-keys")
1778
+
1779
+ # Public key diagnostics
1780
+ pk_sha256 = _hash.sha256(pk_bytes).hexdigest()
1781
+ click.echo(
1782
+ f"[preflight] public_key: b64_len={len(pk)} sha256={pk_sha256} head={pk[:16]} tail={pk[-16:]}"
1783
+ )
1784
+
1785
+ # Plaintext diagnostics (never print full secret)
1786
+ _plain = env_api_key
1787
+ _plen = len(_plain)
1788
+ _ppref = (_plain[:6] + "…") if _plen > 10 else _plain
1789
+ _psuf = ("…" + _plain[-4:]) if _plen > 10 else ""
1790
+ _has_ws = any(ch.isspace() for ch in _plain)
1791
+ click.echo(
1792
+ f"[preflight] plaintext: len={_plen} preview={_ppref}{_psuf} has_ws={bool(_has_ws)}"
1793
+ )
1794
+
1795
+ # Ciphertext diagnostics
1796
+ try:
1797
+ _ct_bytes = base64.b64decode(ct_b64, validate=True)
1798
+ _ct_sha256 = _hash.sha256(_ct_bytes).hexdigest()
1799
+ click.echo(
1800
+ f"[preflight] ciphertext: b64_len={len(ct_b64)} sha256={_ct_sha256} head={ct_b64[:16]} tail={ct_b64[-16:]}"
1801
+ )
1802
+ except Exception:
1803
+ click.echo("[preflight] ciphertext: invalid base64 (unexpected)")
1804
+ except Exception:
1805
+ # Best-effort logging only
1806
+ pass
1807
+ with httpx.Client(
1808
+ timeout=15.0,
1809
+ headers={
1810
+ "Authorization": f"Bearer {synth_key}",
1811
+ "Content-Type": "application/json",
1812
+ },
1813
+ ) as c:
1814
+ click.echo("[preflight] upserting env key…")
1815
+ up = c.post(f"{backend_base.rstrip('/')}/v1/env-keys", json=payload)
1816
+ body_snip = ""
1817
+ try:
1818
+ body_snip = up.text[:400] if up.text else ""
1819
+ except Exception:
1820
+ body_snip = ""
1821
+ click.echo(f"[preflight] upsert status={up.status_code}{(' body='+body_snip) if body_snip else ''}")
1822
+
1823
+ # If upload succeeded (2xx), consider it successful even if verification fails
1824
+ # This handles cases where verification endpoint has issues
1825
+ if 200 <= up.status_code < 300:
1826
+ key_preview = (
1827
+ _preview(env_api_key)
1828
+ )
1829
+ click.echo(
1830
+ f"✅ ENVIRONMENT_API_KEY uploaded successfully ({key_preview})"
1831
+ )
1832
+
1833
+ # Try verification, but don't fail if it doesn't work
1834
+ click.echo("[preflight] verifying env key presence…")
1835
+ try:
1836
+ ver = c.get(f"{backend_base.rstrip('/')}/v1/env-keys/verify")
1837
+ if ver.status_code == 200 and (ver.json() or {}).get("present"):
1838
+ click.echo("✅ Key verified in backend")
1839
+ else:
1840
+ click.echo(
1841
+ f"⚠️ Verification returned {ver.status_code}, but upload succeeded - proceeding"
1842
+ )
1843
+ except Exception as verify_err:
1844
+ click.echo(
1845
+ f"⚠️ Verification check failed ({verify_err}), but upload succeeded - proceeding"
1846
+ )
1847
+ else:
1848
+ error_msg = (
1849
+ f"ENVIRONMENT_API_KEY upload failed with status {up.status_code}"
1850
+ + (f" body={body_snip}" if body_snip else "")
1851
+ )
1852
+ if crash_on_failure:
1853
+ raise click.ClickException(f"[CRITICAL] {error_msg}")
1854
+ click.echo(f"[WARN] {error_msg}; proceeding anyway")
1855
+ except Exception as e:
1856
+ error_msg = f"Failed to encrypt/upload ENVIRONMENT_API_KEY: {e}"
1857
+ if crash_on_failure:
1858
+ raise click.ClickException(f"[CRITICAL] {error_msg}") from e
1859
+ click.echo(f"[WARN] {error_msg}; proceeding anyway")
1860
+ except Exception as e:
1861
+ error_msg = f"Backend preflight for ENVIRONMENT_API_KEY failed: {e}"
1862
+ if crash_on_failure:
1863
+ raise click.ClickException(f"[CRITICAL] {error_msg}") from e
1864
+ click.echo(f"[WARN] {error_msg}; proceeding anyway")
1865
+
1866
+
1867
+ def _run_modal_with_entry(
1868
+ entry: TaskAppEntryType,
1869
+ modal_cfg: ModalDeploymentConfigType,
1870
+ modal_cli: str,
1871
+ modal_name: str | None,
1872
+ env_paths: list[Path],
1873
+ command: str,
1874
+ *,
1875
+ dry_run: bool = False,
1876
+ original_path: Path | None = None,
1877
+ ) -> None:
1878
+ env_paths_list = [Path(p).resolve() for p in env_paths]
1879
+ dotenv_paths = [str(p) for p in env_paths_list]
1880
+ _load_env_files_into_process(dotenv_paths)
1881
+ fallback_dir = env_paths_list[0].parent if env_paths_list else Path.cwd()
1882
+ _ensure_env_values(env_paths_list, fallback_dir)
1883
+ _load_env_values(env_paths_list)
1884
+ _preflight_env_key(env_paths_list, crash_on_failure=True)
1885
+
1886
+ inline_secret_values: dict[str, str] = {}
1887
+ env_key = os.environ.get("ENVIRONMENT_API_KEY", "").strip()
1888
+ if env_key:
1889
+ inline_secret_values["ENVIRONMENT_API_KEY"] = env_key
1890
+ inline_secret_values.setdefault("DEV_ENVIRONMENT_API_KEY", env_key)
1891
+ aliases = os.environ.get("ENVIRONMENT_API_KEY_ALIASES", "").strip()
1892
+ if aliases:
1893
+ inline_secret_values["ENVIRONMENT_API_KEY_ALIASES"] = aliases
1894
+ for vendor_key in ("GROQ_API_KEY", "OPENAI_API_KEY"):
1895
+ val = os.environ.get(vendor_key, "").strip()
1896
+ if val:
1897
+ inline_secret_values[vendor_key] = val
1898
+
1899
+ if inline_secret_values:
1900
+ preview = inline_secret_values.get("ENVIRONMENT_API_KEY", "")
1901
+ shown = f"{preview[:6]}...{preview[-4:]}" if preview and len(preview) > 10 else preview
1902
+ click.echo(f"[deploy] inline ENVIRONMENT_API_KEY prepared ({shown})")
1903
+ else:
1904
+ click.echo("[deploy] no inline ENVIRONMENT_API_KEY found; relying on Modal secrets/dotenv")
1905
+
1906
+ script_path = _write_modal_entrypoint(
1907
+ entry,
1908
+ modal_cfg,
1909
+ modal_name,
1910
+ dotenv_paths=dotenv_paths,
1911
+ original_path=original_path,
1912
+ inline_secret_values=inline_secret_values,
1913
+ )
1914
+ cmd = [*_modal_command_prefix(modal_cli), command, str(script_path)]
1915
+ if modal_name and command == "deploy":
1916
+ cmd.extend(["--name", modal_name])
1917
+
1918
+ proc_env = os.environ.copy()
1919
+ pythonpath_entries: list[str] = [str(REPO_ROOT)]
1920
+ if original_path is not None:
1921
+ source_dir = Path(original_path).resolve().parent
1922
+ pythonpath_entries.insert(0, str(source_dir))
1923
+ existing_pp = proc_env.get("PYTHONPATH")
1924
+ if existing_pp:
1925
+ pythonpath_entries.append(existing_pp)
1926
+ proc_env["PYTHONPATH"] = os.pathsep.join(list(dict.fromkeys(pythonpath_entries)))
1927
+
1928
+ if dry_run:
1929
+ click.echo("Dry run: " + " ".join(shlex.quote(component) for component in cmd))
1930
+ script_path.unlink(missing_ok=True)
1931
+ return
1932
+ click.secho(
1933
+ "[modal-exec] " + " ".join(shlex.quote(component) for component in cmd),
1934
+ fg="cyan",
1935
+ )
1936
+
1937
+ try:
1938
+ # Stream output live for better diagnostics
1939
+ proc = subprocess.Popen(
1940
+ cmd,
1941
+ stdout=subprocess.PIPE,
1942
+ stderr=subprocess.STDOUT,
1943
+ text=True,
1944
+ bufsize=1,
1945
+ env=proc_env,
1946
+ )
1947
+ task_app_url = None
1948
+ assert proc.stdout is not None
1949
+ for line in proc.stdout:
1950
+ # Echo lines as they arrive
1951
+ click.echo(line, nl=False)
1952
+ # Look for lines containing modal.run URLs
1953
+ if task_app_url is None and ("modal.run" in line and "=>" in line):
1954
+ parts = line.split("=>")
1955
+ if len(parts) >= 2:
1956
+ task_app_url = parts[-1].strip()
1957
+ # Save URL immediately for convenience
1958
+ if task_app_url and env_paths_list:
1959
+ env_file = env_paths_list[0]
1960
+ _save_to_env_file(env_file, "TASK_APP_BASE_URL", task_app_url)
1961
+ click.echo(f"\n✓ Task app URL: {task_app_url}\n")
1962
+ rc = proc.wait()
1963
+ if rc != 0:
1964
+ raise subprocess.CalledProcessError(rc, cmd)
1965
+ except subprocess.CalledProcessError as exc:
1966
+ raise click.ClickException(
1967
+ f"modal {command} failed with exit code {exc.returncode}"
1968
+ ) from exc
1969
+ finally:
1970
+ script_path.unlink(missing_ok=True)
1971
+
1972
+
1973
+ def _load_env_values(paths: list[Path], *, allow_empty: bool = False) -> dict[str, str]:
1974
+ values: dict[str, str] = {}
1975
+ for p in paths:
1976
+ try:
1977
+ content = p.read_text(encoding="utf-8")
1978
+ except FileNotFoundError:
1979
+ continue
1980
+ for line in content.splitlines():
1981
+ if not line or line.lstrip().startswith("#") or "=" not in line:
1982
+ continue
1983
+ key, value = line.split("=", 1)
1984
+ if key and key not in values:
1985
+ values[key.strip()] = value.strip()
1986
+ if not allow_empty and not values:
1987
+ raise click.ClickException("No environment values found")
1988
+ os.environ.update({k: v for k, v in values.items() if k and v})
1989
+ return values
1990
+
1991
+
1992
+ def _interactive_create_env(target_dir: Path) -> Path | None:
1993
+ env_path = (target_dir / ".env").resolve()
1994
+ if env_path.exists():
1995
+ existing = _parse_env_file(env_path)
1996
+ env_api = (existing.get("ENVIRONMENT_API_KEY") or "").strip()
1997
+ if env_api:
1998
+ return env_path
1999
+ click.echo(f"Existing {env_path} is missing ENVIRONMENT_API_KEY. Let's update it.")
2000
+ return _interactive_fill_env(env_path)
2001
+
2002
+ click.echo("No .env found for this task app. Let's create one.")
2003
+ return _interactive_fill_env(env_path)
2004
+
2005
+
2006
+ def _parse_env_file(path: Path) -> dict[str, str]:
2007
+ data: dict[str, str] = {}
2008
+ try:
2009
+ for line in path.read_text(encoding="utf-8").splitlines():
2010
+ if not line or line.lstrip().startswith("#") or "=" not in line:
2011
+ continue
2012
+ key, value = line.split("=", 1)
2013
+ data[key.strip()] = value.strip()
2014
+ except FileNotFoundError:
2015
+ pass
2016
+ return data
2017
+
2018
+
2019
+ def _interactive_fill_env(env_path: Path) -> Path | None:
2020
+ if not sys.stdin.isatty():
2021
+ raise click.ClickException(
2022
+ "ENVIRONMENT_API_KEY missing. Provide --env-file or run `synth-ai setup` in an interactive shell to create one."
2023
+ )
2024
+ existing = _parse_env_file(env_path) if env_path.exists() else {}
2025
+
2026
+ def _prompt(label: str, *, default: str = "", required: bool) -> str | None:
2027
+ while True:
2028
+ try:
2029
+ value = click.prompt(
2030
+ label, default=default, show_default=bool(default) or not required
2031
+ ).strip()
2032
+ except (Abort, EOFError, KeyboardInterrupt):
2033
+ click.echo("Aborted env creation.")
2034
+ return None
2035
+ if value or not required:
2036
+ return value
2037
+ click.echo("This field is required.")
2038
+
2039
+ env_default = existing.get("ENVIRONMENT_API_KEY", "").strip()
2040
+ env_api_key = _prompt("ENVIRONMENT_API_KEY", default=env_default, required=True)
2041
+ if env_api_key is None:
2042
+ return None
2043
+ synth_default = existing.get("SYNTH_API_KEY", "").strip()
2044
+ openai_default = existing.get("OPENAI_API_KEY", "").strip()
2045
+ synth_key = _prompt("SYNTH_API_KEY (optional)", default=synth_default, required=False) or ""
2046
+ openai_key = _prompt("OPENAI_API_KEY (optional)", default=openai_default, required=False) or ""
2047
+
2048
+ lines = [
2049
+ f"ENVIRONMENT_API_KEY={env_api_key}",
2050
+ f"SYNTH_API_KEY={synth_key}",
2051
+ f"OPENAI_API_KEY={openai_key}",
2052
+ ]
2053
+ env_path.parent.mkdir(parents=True, exist_ok=True)
2054
+ env_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
2055
+ click.echo(f"Wrote credentials to {env_path}")
2056
+ return env_path
2057
+
2058
+
2059
+ def _ensure_env_values(env_paths: list[Path], fallback_dir: Path) -> None:
2060
+ if (os.environ.get("ENVIRONMENT_API_KEY") or "").strip():
2061
+ return
2062
+ target = env_paths[0] if env_paths else (fallback_dir / ".env").resolve()
2063
+ click.echo(
2064
+ "⚠️ ENVIRONMENT_API_KEY not set. Run `uvx synth-ai setup`, "
2065
+ "or pass --env-file pointing at a .env with ENVIRONMENT_API_KEY."
2066
+ )
2067
+ result = _interactive_fill_env(target)
2068
+ if result is None:
2069
+ raise click.ClickException("ENVIRONMENT_API_KEY required to continue")
2070
+ # After generating .env, load it and override any previously-empty values
2071
+ _load_env_values([result])
2072
+ if not (os.environ.get("ENVIRONMENT_API_KEY") or "").strip():
2073
+ raise click.ClickException("Failed to load ENVIRONMENT_API_KEY from generated .env")
2074
+
2075
+
2076
+ def _deploy_entry(
2077
+ entry: TaskAppEntryType,
2078
+ modal_name: str | None,
2079
+ dry_run: bool,
2080
+ modal_cli: str,
2081
+ env_file: Sequence[str],
2082
+ original_path: Path | None = None,
2083
+ ) -> None:
2084
+ modal_cfg = entry.modal # type: ignore[attr-defined]
2085
+ if modal_cfg is None:
2086
+ raise click.ClickException(
2087
+ f"Task app '{entry.app_id}' does not define Modal deployment settings"
2088
+ )
2089
+
2090
+ env_paths = _determine_env_files(entry, env_file, original_path=original_path)
2091
+ click.echo("Using env file(s): " + ", ".join(str(p.resolve()) for p in env_paths))
2092
+ _run_modal_with_entry(
2093
+ entry,
2094
+ modal_cfg,
2095
+ modal_cli,
2096
+ modal_name,
2097
+ env_paths,
2098
+ command="deploy",
2099
+ dry_run=dry_run,
2100
+ original_path=original_path,
2101
+ )
2102
+
2103
+
2104
+ def _modal_serve_entry(
2105
+ entry: TaskAppEntryType,
2106
+ modal_name: str | None,
2107
+ modal_cli: str,
2108
+ env_file: Sequence[str],
2109
+ original_path: Path | None = None,
2110
+ ) -> None:
2111
+ modal_cfg = entry.modal # type: ignore[attr-defined]
2112
+ if modal_cfg is None:
2113
+ raise click.ClickException(
2114
+ f"Task app '{entry.app_id}' does not define Modal deployment settings"
2115
+ )
2116
+
2117
+ env_paths = _determine_env_files(entry, env_file, original_path=original_path)
2118
+ click.echo("Using env file(s): " + ", ".join(str(p.resolve()) for p in env_paths))
2119
+ _run_modal_with_entry(
2120
+ entry,
2121
+ modal_cfg,
2122
+ modal_cli,
2123
+ modal_name,
2124
+ env_paths,
2125
+ command="serve",
2126
+ original_path=original_path,
2127
+ )
2128
+
2129
+
2130
+ @click.group(name="task-app-group", help="Utilities for serving and deploying Synth task apps.")
2131
+ def task_app_group() -> None:
2132
+ pass
2133
+
2134
+
2135
+ @task_app_group.command("list")
2136
+ def list_apps() -> None:
2137
+ """List registered task apps."""
2138
+
2139
+ entries = registry.list()
2140
+ if not entries:
2141
+ click.echo("No task apps registered.")
2142
+ return
2143
+ for entry in entries:
2144
+ aliases = f" (aliases: {', '.join(entry.aliases)})" if entry.aliases else ""
2145
+ click.echo(f"- {entry.app_id}{aliases}: {entry.description}")
2146
+
2147
+
2148
+ @task_app_group.command("validate")
2149
+ @click.argument("app_id", type=str, required=True)
2150
+ @click.option(
2151
+ "--url",
2152
+ type=str,
2153
+ default=None,
2154
+ help="Task app URL to validate (if not provided, starts a local server)",
2155
+ )
2156
+ @click.option(
2157
+ "--port",
2158
+ type=int,
2159
+ default=8765,
2160
+ help="Port to use for temporary server (default: 8765)",
2161
+ )
2162
+ @click.option(
2163
+ "--api-key",
2164
+ type=str,
2165
+ default=None,
2166
+ envvar="ENVIRONMENT_API_KEY",
2167
+ help="API key for authentication (default: $ENVIRONMENT_API_KEY)",
2168
+ )
2169
+ @click.option(
2170
+ "--min-instances",
2171
+ type=int,
2172
+ default=10,
2173
+ help="Minimum number of task instances required (default: 10)",
2174
+ )
2175
+ @click.option(
2176
+ "--verbose",
2177
+ "-v",
2178
+ is_flag=True,
2179
+ help="Show detailed information about the task app",
2180
+ )
2181
+ @click.option(
2182
+ "--json",
2183
+ "output_json",
2184
+ is_flag=True,
2185
+ help="Output results as JSON",
2186
+ )
2187
+ def validate_task_app_cmd(
2188
+ app_id: str,
2189
+ url: str | None,
2190
+ port: int,
2191
+ api_key: str | None,
2192
+ min_instances: int,
2193
+ verbose: bool,
2194
+ output_json: bool,
2195
+ ) -> None:
2196
+ """Validate a task app deployment readiness.
2197
+
2198
+ This command verifies that a task app is properly configured and ready to run
2199
+ by checking all required HTTP endpoints, authentication, and task availability.
2200
+
2201
+ By default, it starts a temporary local server for validation. You can also
2202
+ validate a remote deployment by passing --url.
2203
+
2204
+ \b
2205
+ What gets validated:
2206
+ • Root endpoint (/) responds correctly
2207
+ • Health endpoint (/health) is accessible with proper authentication
2208
+ • Info endpoint (/info) returns valid task metadata
2209
+ • Task info endpoint (/task_info) provides task instances
2210
+ • Rollout endpoint (/rollout) is registered
2211
+ • At least N task instances are available (default: 10)
2212
+
2213
+ \b
2214
+ Examples:
2215
+
2216
+ \b
2217
+ Validate grpo-crafter (starts local server automatically):
2218
+ $ synth-ai task-app validate grpo-crafter
2219
+
2220
+ \b
2221
+ Validate sokoban with verbose output:
2222
+ $ synth-ai task-app validate sokoban --verbose
2223
+
2224
+ \b
2225
+ Validate with custom port:
2226
+ $ synth-ai task-app validate sokoban --port 9000
2227
+
2228
+ \b
2229
+ Validate a remote deployment:
2230
+ $ synth-ai task-app validate grpo-crafter --url https://my-crafter.modal.run
2231
+
2232
+ \b
2233
+ Require at least 20 task instances:
2234
+ $ synth-ai task-app validate grpo-crafter --min-instances 20
2235
+
2236
+ \b
2237
+ Get JSON output for automation:
2238
+ $ synth-ai task-app validate sokoban --json
2239
+
2240
+ \b
2241
+ Common use cases:
2242
+ • Pre-deployment verification: Check task app works before deploying to Modal
2243
+ • CI/CD integration: Use --json flag for automated validation in pipelines
2244
+ • Debug failing deployments: Use --verbose to see detailed endpoint responses
2245
+ • Test API key configuration: Verify authentication is set up correctly
2246
+ """
2247
+ import socket
2248
+ import subprocess
2249
+ import tempfile
2250
+ import time
2251
+
2252
+ # Import the validate_task_app function defined in this module
2253
+ from ._validate_task_app import validate_task_app # type: ignore[attr-defined]
2254
+
2255
+ proc = None
2256
+ task_app_url = url
2257
+
2258
+ try:
2259
+ # If no URL provided, start a temporary server
2260
+ if not task_app_url:
2261
+ # Find an available port
2262
+ def is_port_available(port: int) -> bool:
2263
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
2264
+ try:
2265
+ s.bind(("", port))
2266
+ return True
2267
+ except OSError:
2268
+ return False
2269
+
2270
+ while not is_port_available(port):
2271
+ port += 1
2272
+
2273
+ task_app_url = f"http://localhost:{port}"
2274
+
2275
+ if not output_json:
2276
+ click.echo(f"Starting temporary {app_id} server on port {port}...")
2277
+
2278
+ # Start the server in background
2279
+ env = os.environ.copy()
2280
+ if api_key:
2281
+ env["ENVIRONMENT_API_KEY"] = api_key
2282
+
2283
+ # Create a temporary trace DB and trace dir to avoid prompts
2284
+ import tempfile
2285
+ temp_dir = tempfile.mkdtemp()
2286
+ temp_trace_db = os.path.join(temp_dir, "validate_trace.db")
2287
+ temp_trace_dir = os.path.join(temp_dir, "traces")
2288
+ os.makedirs(temp_trace_dir, exist_ok=True)
2289
+
2290
+ proc = subprocess.Popen(
2291
+ [
2292
+ "uv",
2293
+ "run",
2294
+ "synth-ai",
2295
+ "task-app",
2296
+ "serve",
2297
+ app_id,
2298
+ "--port",
2299
+ str(port),
2300
+ "--no-reload",
2301
+ "--trace",
2302
+ temp_trace_dir,
2303
+ "--trace-db",
2304
+ temp_trace_db,
2305
+ ],
2306
+ env=env,
2307
+ stdin=subprocess.PIPE, # Add stdin to handle any prompts
2308
+ stdout=subprocess.DEVNULL if output_json else subprocess.PIPE,
2309
+ stderr=subprocess.DEVNULL if output_json else subprocess.PIPE,
2310
+ text=True,
2311
+ )
2312
+
2313
+ # Write empty input to stdin to skip any prompts
2314
+ if proc.stdin:
2315
+ try:
2316
+ proc.stdin.write("\n")
2317
+ proc.stdin.flush()
2318
+ proc.stdin.close()
2319
+ except Exception:
2320
+ pass
2321
+
2322
+ # Wait for server to be ready
2323
+ if not output_json:
2324
+ click.echo("Waiting for server to start...")
2325
+
2326
+ import httpx
2327
+ for _attempt in range(60): # 30 seconds timeout
2328
+ try:
2329
+ async def check_health():
2330
+ async with httpx.AsyncClient(timeout=2.0) as client:
2331
+ resp = await client.get(f"{task_app_url}/")
2332
+ return resp.status_code == 200
2333
+
2334
+ if asyncio.run(check_health()):
2335
+ break
2336
+ except Exception:
2337
+ pass
2338
+
2339
+ # Check if process died
2340
+ if proc.poll() is not None:
2341
+ stderr_output = ""
2342
+ if proc.stderr and not output_json:
2343
+ stderr_output = proc.stderr.read()
2344
+ click.echo(click.style("✗ Server process exited unexpectedly", fg="red"), err=True)
2345
+ if stderr_output and not output_json:
2346
+ click.echo(f"Error output:\n{stderr_output}", err=True)
2347
+ sys.exit(1)
2348
+
2349
+ time.sleep(0.5)
2350
+ else:
2351
+ click.echo(click.style("✗ Server failed to start within 30 seconds", fg="red"), err=True)
2352
+ sys.exit(1)
2353
+
2354
+ if not output_json:
2355
+ click.echo(click.style("✓ Server started", fg="green"))
2356
+ click.echo()
2357
+
2358
+ # Ensure URL doesn't have trailing slash
2359
+ task_app_url = task_app_url.rstrip("/")
2360
+
2361
+ async def _run() -> tuple[bool, dict[str, Any]]:
2362
+ return await validate_task_app(
2363
+ url=task_app_url,
2364
+ api_key=api_key,
2365
+ min_instances=min_instances,
2366
+ verbose=verbose,
2367
+ )
2368
+
2369
+ success, results = asyncio.run(_run())
2370
+
2371
+ if output_json:
2372
+ import json as _json
2373
+ click.echo(_json.dumps(results, indent=2))
2374
+
2375
+ sys.exit(0 if success else 1)
2376
+
2377
+ finally:
2378
+ # Cleanup: stop the temporary server
2379
+ if proc is not None:
2380
+ if not output_json:
2381
+ click.echo("\nStopping temporary server...")
2382
+ try:
2383
+ proc.terminate()
2384
+ proc.wait(timeout=5)
2385
+ except Exception:
2386
+ proc.kill()
2387
+
2388
+ # Cleanup temp trace DB
2389
+ if not url and 'temp_dir' in locals():
2390
+ import contextlib
2391
+ import shutil
2392
+ with contextlib.suppress(Exception):
2393
+ shutil.rmtree(temp_dir, ignore_errors=True)
2394
+
2395
+
2396
+ def _load_env_files_into_process(paths: Sequence[str]) -> None:
2397
+ for p in paths:
2398
+ try:
2399
+ txt = Path(p).expanduser().read_text()
2400
+ except Exception:
2401
+ continue
2402
+ for line in txt.splitlines():
2403
+ if not line or line.startswith("#") or "=" not in line:
2404
+ continue
2405
+ k, v = line.split("=", 1)
2406
+ key = k.strip()
2407
+ val = v.strip().strip('"').strip("'")
2408
+ # Load into process, but allow overriding if the current value is empty
2409
+ if key:
2410
+ current = os.environ.get(key)
2411
+ if current is None or not str(current).strip():
2412
+ os.environ[key] = val
2413
+
2414
+
2415
+ @click.command("serve")
2416
+ @click.argument("app_id", type=str, required=False)
2417
+ @click.option("--host", default="0.0.0.0", show_default=True)
2418
+ @click.option("--port", default=None, type=int, help="Port to serve on (default: 8001)")
2419
+ @click.option("--env-file", multiple=True, type=click.Path(), help="Extra .env files to load")
2420
+ @click.option(
2421
+ "--reload/--no-reload", "reload_flag", default=False, help="Enable uvicorn auto-reload"
2422
+ )
2423
+ @click.option(
2424
+ "--force/--no-force",
2425
+ "force",
2426
+ default=False,
2427
+ help="Kill any process already bound to the selected port before starting",
2428
+ )
2429
+ @click.option(
2430
+ "--trace",
2431
+ "trace_dir",
2432
+ type=click.Path(),
2433
+ default=None,
2434
+ help="Enable tracing and write SFT JSONL files to this directory (default: traces/v3)",
2435
+ )
2436
+ @click.option(
2437
+ "--trace-db",
2438
+ "trace_db",
2439
+ type=click.Path(),
2440
+ default=None,
2441
+ help="Override local trace DB path (default: traces/v3/synth_ai.db)",
2442
+ )
2443
+ def serve_command(
2444
+ app_id: str | None,
2445
+ host: str,
2446
+ port: int | None,
2447
+ env_file: Sequence[str],
2448
+ reload_flag: bool,
2449
+ force: bool,
2450
+ trace_dir: str | None,
2451
+ trace_db: str | None,
2452
+ ) -> None:
2453
+ return None
2454
+
2455
+
2456
+ @task_app_group.command("info")
2457
+ @click.option(
2458
+ "--base",
2459
+ "base_url",
2460
+ default=None,
2461
+ help="Task app base URL (default: TASK_APP_BASE_URL or http://127.0.0.1:8001)",
2462
+ )
2463
+ @click.option(
2464
+ "--api-key",
2465
+ default=None,
2466
+ help="Environment API key (default: ENVIRONMENT_API_KEY or dev fallbacks)",
2467
+ )
2468
+ @click.option(
2469
+ "--seed",
2470
+ "seeds",
2471
+ multiple=True,
2472
+ type=int,
2473
+ help="Optional seed(s) to request specific instances (repeatable)",
2474
+ )
2475
+ def info_command(base_url: str | None, api_key: str | None, seeds: tuple[int, ...]) -> None:
2476
+ """Fetch Task App /task_info with authentication and print JSON."""
2477
+ import json as _json
2478
+ import os as _os
2479
+
2480
+ import requests as _requests
2481
+
2482
+ base = (base_url or _os.getenv("TASK_APP_BASE_URL") or "http://127.0.0.1:8001").rstrip("/")
2483
+
2484
+ # Resolve API key, permitting dev fallbacks
2485
+ auth_module = _maybe_import("synth_ai.sdk.task.auth")
2486
+ if auth_module is not None:
2487
+ _norm_key = getattr(auth_module, "normalize_environment_api_key", lambda: _os.getenv("ENVIRONMENT_API_KEY"))
2488
+ else:
2489
+ _norm_key = lambda: _os.getenv("ENVIRONMENT_API_KEY") # noqa: E731
2490
+ key = (api_key or _norm_key() or "").strip()
2491
+ if not key:
2492
+ raise click.ClickException("Missing API key. Provide --api-key or set ENVIRONMENT_API_KEY.")
2493
+
2494
+ headers: dict[str, str] = {"X-API-Key": key, "Authorization": f"Bearer {key}"}
2495
+ aliases = (_os.getenv("ENVIRONMENT_API_KEY_ALIASES") or "").strip()
2496
+ keys_csv = (
2497
+ ",".join([key] + [p.strip() for p in aliases.split(",") if p.strip()]) if aliases else key
2498
+ )
2499
+ if keys_csv:
2500
+ headers["X-API-Keys"] = keys_csv
2501
+
2502
+ params: list[tuple[str, str]] = []
2503
+ for s in seeds:
2504
+ params.append(("seed", str(int(s))))
2505
+
2506
+ url = f"{base}/task_info"
2507
+ try:
2508
+ r = _requests.get(url, headers=headers, params=params or None, timeout=30)
2509
+ except Exception as exc:
2510
+ raise click.ClickException(f"Request failed: {exc}") from exc
2511
+ if not (200 <= r.status_code < 300):
2512
+ ct = r.headers.get("content-type", "")
2513
+ detail = r.text
2514
+ if ct.startswith("application/json"):
2515
+ with contextlib.suppress(Exception):
2516
+ detail = _json.dumps(r.json(), indent=2)
2517
+ raise click.ClickException(f"{url} returned {r.status_code}:\n{detail}")
2518
+
2519
+ data = (
2520
+ r.json()
2521
+ if r.headers.get("content-type", "").startswith("application/json")
2522
+ else {"raw": r.text}
2523
+ )
2524
+ click.echo(_json.dumps(data, indent=2, sort_keys=True))
2525
+
2526
+
2527
+ @task_app_group.command("serve")
2528
+ @click.argument("app_id", type=str, required=False)
2529
+ @click.option("--host", default="0.0.0.0", show_default=True)
2530
+ @click.option("--port", default=None, type=int, help="Port to serve on (default: 8001)")
2531
+ @click.option("--env-file", multiple=True, type=click.Path(), help="Extra .env files to load")
2532
+ @click.option(
2533
+ "--reload/--no-reload", "reload_flag", default=False, help="Enable uvicorn auto-reload"
2534
+ )
2535
+ @click.option(
2536
+ "--force/--no-force",
2537
+ "force",
2538
+ default=False,
2539
+ help="Kill any process already bound to the selected port before starting",
2540
+ )
2541
+ @click.option(
2542
+ "--trace",
2543
+ "trace_dir",
2544
+ type=click.Path(),
2545
+ default=None,
2546
+ help="Enable tracing and write SFT JSONL files to this directory (default: traces/v3)",
2547
+ )
2548
+ @click.option(
2549
+ "--trace-db",
2550
+ "trace_db",
2551
+ type=click.Path(),
2552
+ default=None,
2553
+ help="Override local trace DB path (default: traces/v3/synth_ai.db)",
2554
+ )
2555
+ def serve_task_group(
2556
+ app_id: str | None,
2557
+ host: str,
2558
+ port: int | None,
2559
+ env_file: Sequence[str],
2560
+ reload_flag: bool,
2561
+ force: bool,
2562
+ trace_dir: str | None,
2563
+ trace_db: str | None,
2564
+ ) -> None:
2565
+ """Serve a TaskAppConfig-based task app using uvicorn."""
2566
+ import contextlib
2567
+
2568
+ if not host:
2569
+ host = "0.0.0.0"
2570
+
2571
+ if port is None:
2572
+ port = 8001
2573
+
2574
+ # Auto-enable tracing by default
2575
+ try:
2576
+ auto_trace = os.getenv("SYNTH_AUTO_TRACE", "1")
2577
+ auto_trace_enabled = auto_trace not in {"0", "false", "False", ""}
2578
+ except Exception:
2579
+ auto_trace_enabled = True
2580
+
2581
+ if auto_trace_enabled:
2582
+ demo_base = Path(os.environ.get("SYNTH_DEMO_DIR") or Path.cwd())
2583
+ if trace_dir is None:
2584
+ default_trace_dir = (demo_base / "traces" / "v3").resolve()
2585
+ with contextlib.suppress(Exception):
2586
+ default_trace_dir.mkdir(parents=True, exist_ok=True)
2587
+ trace_dir = str(default_trace_dir)
2588
+ click.echo(f"[trace] Using trace directory: {trace_dir}")
2589
+ if trace_dir and trace_db is None:
2590
+ default_trace_db = (Path(trace_dir) / "synth_ai.db").resolve()
2591
+ with contextlib.suppress(Exception):
2592
+ default_trace_db.parent.mkdir(parents=True, exist_ok=True)
2593
+ trace_db = str(default_trace_db)
2594
+ click.echo(f"[trace] Using trace DB: {trace_db}")
2595
+
2596
+ # Select and serve the app
2597
+ choice = _select_app_choice(app_id, purpose="serve")
2598
+ entry = choice.ensure_entry()
2599
+ _serve_entry(
2600
+ entry,
2601
+ host,
2602
+ port,
2603
+ env_file,
2604
+ reload_flag,
2605
+ force,
2606
+ trace_dir=trace_dir,
2607
+ trace_db=trace_db,
2608
+ )
2609
+
2610
+
2611
+
2612
+ def _determine_env_files(
2613
+ entry: TaskAppEntryType, user_env_files: Sequence[str], *, original_path: Path | None = None
2614
+ ) -> list[Path]:
2615
+ resolved: list[Path] = []
2616
+ for candidate in user_env_files:
2617
+ p = Path(candidate).expanduser()
2618
+ if not p.exists():
2619
+ raise click.ClickException(f"Env file not found: {p}")
2620
+ resolved.append(p)
2621
+ if resolved:
2622
+ return resolved
2623
+
2624
+ declared: list[Path] = []
2625
+ for candidate in getattr(entry, "env_files", ()) or ():
2626
+ try:
2627
+ p = Path(candidate).expanduser()
2628
+ except Exception:
2629
+ continue
2630
+ if p.exists() and p.is_file():
2631
+ declared.append(p)
2632
+ if declared:
2633
+ return declared
2634
+
2635
+ def _append_candidate(collection: list[Path], candidate: Path) -> None:
2636
+ if candidate.exists() and candidate.is_file() and candidate not in collection:
2637
+ collection.append(candidate)
2638
+
2639
+ auto_candidates: list[Path] = []
2640
+
2641
+ search_dirs: list[Path] = []
2642
+ if original_path is not None:
2643
+ search_dirs.append(original_path.parent.resolve())
2644
+ for parent in original_path.parent.resolve().parents:
2645
+ search_dirs.append(parent)
2646
+ cwd = Path.cwd().resolve()
2647
+ if cwd not in search_dirs:
2648
+ search_dirs.append(cwd)
2649
+ repo_root = REPO_ROOT.resolve()
2650
+ if repo_root not in search_dirs:
2651
+ search_dirs.append(repo_root)
2652
+
2653
+ for directory in search_dirs:
2654
+ _append_candidate(auto_candidates, directory / ".env")
2655
+ for candidate in sorted(directory.glob("*.env")):
2656
+ _append_candidate(auto_candidates, candidate)
2657
+
2658
+ if auto_candidates:
2659
+ return [auto_candidates[0]]
2660
+
2661
+ raise click.ClickException(
2662
+ "No .env file discovered automatically. Pass --env-file /path/to/.env or generate one with `uvx synth-ai setup`."
2663
+ )
2664
+
2665
+
2666
+ def _ensure_port_free(port: int, host: str, *, force: bool) -> None:
2667
+ import os
2668
+ import socket
2669
+ import subprocess
2670
+ import time
2671
+
2672
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
2673
+ in_use = s.connect_ex((host, port)) == 0
2674
+ if not in_use:
2675
+ return
2676
+
2677
+ try:
2678
+ out = subprocess.run(
2679
+ ["lsof", "-ti", f"TCP:{port}"], capture_output=True, text=True, check=False
2680
+ )
2681
+ pids = [pid for pid in out.stdout.strip().splitlines() if pid]
2682
+ except FileNotFoundError:
2683
+ pids = []
2684
+
2685
+ if not force:
2686
+ message = f"Port {port} appears to be in use"
2687
+ if pids:
2688
+ message += f" (PIDs: {', '.join(pids)})"
2689
+ raise click.ClickException(message)
2690
+
2691
+ for pid in pids:
2692
+ try:
2693
+ os.kill(int(pid), signal.SIGTERM)
2694
+ except Exception as exc:
2695
+ raise click.ClickException(f"Failed to terminate PID {pid}: {exc}") from exc
2696
+
2697
+ time.sleep(0.5)
2698
+
2699
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
2700
+ still_in_use = s.connect_ex((host, port)) == 0
2701
+
2702
+ if still_in_use:
2703
+ for pid in pids:
2704
+ try:
2705
+ os.kill(int(pid), signal.SIGKILL)
2706
+ except Exception as exc:
2707
+ raise click.ClickException(f"Failed to force terminate PID {pid}: {exc}") from exc
2708
+ time.sleep(0.5)
2709
+
2710
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
2711
+ in_use_after = s.connect_ex((host, port)) == 0
2712
+ if in_use_after:
2713
+ raise click.ClickException(
2714
+ f"Port {port} is still in use after attempting to terminate processes."
2715
+ )
2716
+
2717
+
2718
+ def _save_to_env_file(env_path: Path, key: str, value: str) -> None:
2719
+ """Save or update a key-value pair in the .env file."""
2720
+ try:
2721
+ # Read existing .env
2722
+ existing_lines = []
2723
+ if env_path.exists():
2724
+ existing_lines = env_path.read_text().splitlines()
2725
+ else:
2726
+ env_path.parent.mkdir(parents=True, exist_ok=True)
2727
+
2728
+ # Check if key already exists and update it
2729
+ key_updated = False
2730
+ new_lines = []
2731
+ for line in existing_lines:
2732
+ if line.strip().startswith(f"{key}="):
2733
+ new_lines.append(f"{key}={value}")
2734
+ key_updated = True
2735
+ else:
2736
+ new_lines.append(line)
2737
+
2738
+ if key_updated:
2739
+ # Write updated lines back
2740
+ env_path.write_text("\n".join(new_lines) + "\n")
2741
+ click.echo(f"Updated {key} in {env_path}")
2742
+ else:
2743
+ # Append to .env
2744
+ with open(env_path, "a") as f:
2745
+ if existing_lines and not existing_lines[-1].strip():
2746
+ # File exists and last line is not empty
2747
+ pass
2748
+ elif existing_lines:
2749
+ # Add newline before appending
2750
+ f.write("\n")
2751
+ f.write(f"{key}={value}\n")
2752
+ click.echo(f"Saved {key} to {env_path}")
2753
+ except Exception as e:
2754
+ click.echo(f"Warning: Could not save {key} to .env: {e}", err=True)
2755
+
2756
+
2757
+ def _persist_env_api_key(env_api_key: str, env_paths: Sequence[Path] | None) -> None:
2758
+ """Persist ENVIRONMENT_API_KEY to provided env files (or default .env)."""
2759
+ targets: list[Path] = []
2760
+ seen: set[Path] = set()
2761
+ for path in env_paths or ():
2762
+ try:
2763
+ resolved = Path(path).resolve()
2764
+ except Exception:
2765
+ continue
2766
+ if resolved in seen:
2767
+ continue
2768
+ seen.add(resolved)
2769
+ targets.append(resolved)
2770
+
2771
+ if not targets:
2772
+ demo_dir = Path(os.environ.get("SYNTH_DEMO_DIR") or Path.cwd())
2773
+ targets.append((demo_dir / ".env").resolve())
2774
+
2775
+ for target in targets:
2776
+ _save_to_env_file(target, "ENVIRONMENT_API_KEY", env_api_key)
2777
+
2778
+
2779
+ def _validate_required_env_keys() -> None:
2780
+ """Validate required environment keys are set, prompting if missing."""
2781
+ # Use demo directory .env file if set, otherwise current directory
2782
+ demo_base = Path(os.environ.get("SYNTH_DEMO_DIR") or Path.cwd())
2783
+ env_file = demo_base / ".env"
2784
+
2785
+ if env_file.exists():
2786
+ try:
2787
+ from dotenv import load_dotenv
2788
+
2789
+ load_dotenv(env_file, override=False)
2790
+ except Exception:
2791
+ pass # Best effort
2792
+
2793
+ env_api_key = os.environ.get("ENVIRONMENT_API_KEY", "").strip()
2794
+
2795
+ if not env_api_key:
2796
+ env_api_key = input("Please enter your RL Environment API key:\n> ").strip()
2797
+ if not env_api_key:
2798
+ raise click.ClickException("RL Environment API key is required to start the server")
2799
+ os.environ["ENVIRONMENT_API_KEY"] = env_api_key
2800
+ _save_to_env_file(env_file, "ENVIRONMENT_API_KEY", env_api_key)
2801
+
2802
+ # Check for Groq API key
2803
+ groq_api_key = os.environ.get("GROQ_API_KEY", "").strip()
2804
+
2805
+ if not groq_api_key:
2806
+ click.echo("\nInference API key configuration:")
2807
+ click.echo("This workflow requires a Groq API key.")
2808
+ groq_api_key = input("Groq API key (or press Enter to skip): ").strip()
2809
+ if groq_api_key:
2810
+ os.environ["GROQ_API_KEY"] = groq_api_key
2811
+ _save_to_env_file(env_file, "GROQ_API_KEY", groq_api_key)
2812
+
2813
+
2814
+ def _print_demo_next_steps_if_applicable() -> None:
2815
+ """Print next steps if currently in a demo directory."""
2816
+ try:
2817
+ cwd = Path.cwd().resolve()
2818
+ demo_dir = _load_demo_directory()
2819
+
2820
+ if demo_dir and demo_dir == cwd and (cwd / "run_local_rollout_traced.py").exists():
2821
+ click.echo("\n" + "=" * 60)
2822
+ click.echo("Next step: Collect traced rollouts")
2823
+ click.echo("=" * 60)
2824
+ click.echo("\nIn another terminal, run:")
2825
+ click.echo(f" cd {cwd}")
2826
+ click.echo(" uv run python run_local_rollout_traced.py")
2827
+ click.echo("\nRun this 5-10 times to collect diverse traces.")
2828
+ click.echo("=" * 60 + "\n")
2829
+ except Exception:
2830
+ pass
2831
+
2832
+
2833
+ def _serve_entry(
2834
+ entry: TaskAppEntryType,
2835
+ host: str,
2836
+ port: int,
2837
+ env_file: Sequence[str],
2838
+ reload_flag: bool,
2839
+ force: bool,
2840
+ *,
2841
+ trace_dir: str | None = None,
2842
+ trace_db: str | None = None,
2843
+ ) -> None:
2844
+ env_files = list(entry.env_files)
2845
+ env_files.extend(env_file)
2846
+
2847
+ trace_enabled = trace_dir is not None or trace_db is not None
2848
+ if trace_enabled:
2849
+ os.environ["TASKAPP_TRACING_ENABLED"] = "1"
2850
+
2851
+ # Ensure paths are absolute relative to demo directory
2852
+ demo_base = Path(os.environ.get("SYNTH_DEMO_DIR") or Path.cwd())
2853
+
2854
+ if trace_dir is not None:
2855
+ dir_path = Path(trace_dir).expanduser()
2856
+ if not dir_path.is_absolute():
2857
+ dir_path = (demo_base / dir_path).resolve()
2858
+ try:
2859
+ dir_path.mkdir(parents=True, exist_ok=True)
2860
+ except Exception as exc:
2861
+ raise click.ClickException(
2862
+ f"Failed to create trace directory {dir_path}: {exc}"
2863
+ ) from exc
2864
+ os.environ["TASKAPP_SFT_OUTPUT_DIR"] = str(dir_path)
2865
+ click.echo(f"Tracing enabled. SFT JSONL will be written to {dir_path}")
2866
+ if trace_db is not None:
2867
+ db_path = Path(trace_db).expanduser()
2868
+ if not db_path.is_absolute():
2869
+ db_path = (demo_base / db_path).resolve()
2870
+ # Construct the sqlite URL from the absolute path
2871
+ db_url = f"sqlite+aiosqlite:///{db_path}"
2872
+ os.environ["SQLD_DB_PATH"] = str(db_path)
2873
+ os.environ["TURSO_LOCAL_DB_URL"] = db_url
2874
+ click.echo(f"Tracing DB path set to {db_path}")
2875
+ tracing_config_module = _maybe_import("synth_ai.core.tracing_v3.config")
2876
+ if tracing_config_module is not None:
2877
+ trace_config = tracing_config_module.CONFIG
2878
+ new_db_url = os.getenv("TURSO_LOCAL_DB_URL") or trace_config.db_url
2879
+ trace_config.db_url = new_db_url
2880
+ if new_db_url:
2881
+ click.echo(f"Tracing DB URL resolved to {new_db_url}")
2882
+ elif os.getenv("TASKAPP_TRACING_ENABLED"):
2883
+ click.echo("Tracing enabled via environment variables")
2884
+
2885
+ _ensure_port_free(port, host, force=force)
2886
+
2887
+ _validate_required_env_keys()
2888
+ env_path_objs = [Path(p) for p in env_files if p]
2889
+ _preflight_env_key(env_path_objs)
2890
+
2891
+ # Print next steps if in demo context
2892
+ if trace_enabled:
2893
+ _print_demo_next_steps_if_applicable()
2894
+
2895
+ run_task_app(
2896
+ entry.config_factory,
2897
+ host=host,
2898
+ port=port,
2899
+ reload=reload_flag,
2900
+ env_files=env_files,
2901
+ )
2902
+
2903
+
2904
+ def _write_modal_entrypoint(
2905
+ entry: TaskAppEntryType,
2906
+ modal_cfg: ModalDeploymentConfigType,
2907
+ override_name: str | None,
2908
+ *,
2909
+ dotenv_paths: Sequence[str] | None = None,
2910
+ original_path: Path | None = None,
2911
+ inline_secret_values: dict[str, str] | None = None,
2912
+ ) -> Path:
2913
+ modal_name = override_name or modal_cfg.app_name
2914
+
2915
+ # For dynamically discovered apps, import the module by its package path
2916
+ # Compute the module name relative to the mounted repo root (/opt/synth_ai_repo)
2917
+ remote_file_str: str | None = None
2918
+ if original_path:
2919
+ try:
2920
+ # Build lookup of local->remote mounts
2921
+ mount_map: list[tuple[Path, Path]] = [
2922
+ (Path(local).resolve(), Path(remote))
2923
+ for (local, remote) in modal_cfg.extra_local_dirs
2924
+ ]
2925
+ orig = Path(original_path).resolve()
2926
+ for local_src, remote_dst in mount_map:
2927
+ with contextlib.suppress(Exception):
2928
+ if orig.is_relative_to(local_src): # py311+
2929
+ remote_file_str = str((remote_dst / orig.relative_to(local_src)).resolve())
2930
+ break
2931
+ try:
2932
+ rel = orig.relative_to(local_src)
2933
+ remote_file_str = str((remote_dst / rel).resolve())
2934
+ break
2935
+ except Exception:
2936
+ pass
2937
+ except Exception:
2938
+ remote_file_str = None
2939
+ module_name = entry.config_factory.__module__
2940
+
2941
+ # Prefer a guaranteed mount for the discovered file to avoid package import issues
2942
+ guaranteed_file_str: str | None = None
2943
+ if original_path:
2944
+ guaranteed_file_str = str(
2945
+ (Path("/opt/synth_ai_repo/__local_task_app__") / Path(original_path).stem).with_suffix(
2946
+ ".py"
2947
+ )
2948
+ )
2949
+
2950
+ dotenv_paths = [str(Path(path)) for path in (dotenv_paths or [])]
2951
+
2952
+ pip_packages = list(modal_cfg.pip_packages)
2953
+ # Ensure synth-ai (matching host version if available) is installed in the container
2954
+ synth_pkg = "synth-ai"
2955
+ host_synth = _maybe_import("synth_ai")
2956
+ if host_synth is not None:
2957
+ host_ver = getattr(host_synth, "__version__", None)
2958
+ if host_ver:
2959
+ synth_pkg = f"synth-ai=={host_ver}"
2960
+ if not any(str(p).startswith("synth-ai") for p in pip_packages):
2961
+ pip_packages.insert(0, synth_pkg)
2962
+
2963
+ apt_packages = list(modal_cfg.apt_packages)
2964
+ click.echo(f"[DEBUG] modal_cfg.apt_packages type: {type(modal_cfg.apt_packages)}")
2965
+ click.echo(f"[DEBUG] modal_cfg.apt_packages value: {modal_cfg.apt_packages}")
2966
+ click.echo(f"[DEBUG] apt_packages after list(): {apt_packages}")
2967
+
2968
+ local_dirs = [(str(Path(src)), dst) for src, dst in modal_cfg.extra_local_dirs]
2969
+ # Also mount the host synth_ai source if available to ensure latest code is used
2970
+ if host_synth is not None:
2971
+ try:
2972
+ host_synth_dir = Path(host_synth.__file__).resolve().parent
2973
+ sy_dst = "/opt/synth_ai_repo/synth_ai"
2974
+ candidate = (str(host_synth_dir), sy_dst)
2975
+ if candidate not in local_dirs:
2976
+ local_dirs.insert(0, candidate)
2977
+ except Exception:
2978
+ pass
2979
+ # Ensure the discovered app directory is mounted, regardless of modal_cfg
2980
+ if original_path:
2981
+ discovered_dir = str(Path(original_path).resolve().parent)
2982
+ mount_dst = "/opt/synth_ai_repo/__local_task_app__"
2983
+ if (discovered_dir, mount_dst) not in local_dirs:
2984
+ local_dirs.append((discovered_dir, mount_dst))
2985
+ secret_names = list(modal_cfg.secret_names)
2986
+ volume_mounts = [(name, mount) for name, mount in modal_cfg.volume_mounts]
2987
+ inline_secret_values = {k: v for k, v in (inline_secret_values or {}).items() if v}
2988
+
2989
+ script = f"""from __future__ import annotations
2990
+
2991
+ import importlib
2992
+ import importlib.util
2993
+ import sys
2994
+ import os
2995
+ import shutil
2996
+ import tempfile
2997
+ from pathlib import Path as _Path
2998
+ import fnmatch
2999
+ sys.path.insert(0, '/opt/synth_ai_repo')
3000
+
3001
+ from modal import App, Image, Secret, Volume, asgi_app
3002
+
3003
+ # Defer importing synth_ai until inside fastapi_app to avoid local import errors
3004
+
3005
+ ENTRY_ID = {entry.app_id!r}
3006
+ MODAL_APP_NAME = {modal_name!r}
3007
+ MODULE_NAME = {module_name!r}
3008
+ MODULE_FILE = {guaranteed_file_str or remote_file_str!r}
3009
+ DOTENV_PATHS = {dotenv_paths!r}
3010
+ INLINE_SECRET_VALUES = {inline_secret_values!r}
3011
+
3012
+ image = Image.debian_slim(python_version={modal_cfg.python_version!r})
3013
+
3014
+ # CRITICAL: Install iverilog for Verilog task app (hardcoded to prevent config issues)
3015
+ if {entry.app_id!r} == "grpo-verilog":
3016
+ image = image.apt_install("iverilog")
3017
+
3018
+ # Install apt packages first (before pip)
3019
+ apt_packages = {apt_packages!r}
3020
+ if apt_packages:
3021
+ image = image.apt_install(*apt_packages)
3022
+
3023
+ pip_packages = {pip_packages!r}
3024
+ if pip_packages:
3025
+ image = image.pip_install(*pip_packages)
3026
+
3027
+ local_dirs = {local_dirs!r}
3028
+
3029
+ def _copy_tree_filtered(src_dir: str) -> str:
3030
+ src = _Path(src_dir)
3031
+ temp_dir = _Path(tempfile.mkdtemp(prefix='synth_mount_'))
3032
+
3033
+ exclude_dirs = {".cache", ".git", "__pycache__"}
3034
+ exclude_globs = ['*.db', '*.db-journal', '*-wal', '*-shm']
3035
+
3036
+ for root, dirs, files in os.walk(src):
3037
+ rel_root = _Path(root).relative_to(src)
3038
+ # filter dirs in-place
3039
+ dirs[:] = [d for d in dirs if d not in exclude_dirs]
3040
+ # ensure target directory exists
3041
+ target_dir = (temp_dir / rel_root)
3042
+ target_dir.mkdir(parents=True, exist_ok=True)
3043
+ # copy files with filtering
3044
+ for name in files:
3045
+ if any(fnmatch.fnmatch(name, pat) for pat in exclude_globs):
3046
+ continue
3047
+ src_file = _Path(root) / name
3048
+ dst_file = target_dir / name
3049
+ try:
3050
+ shutil.copy2(src_file, dst_file)
3051
+ except Exception:
3052
+ # ignore problematic files
3053
+ continue
3054
+ return str(temp_dir)
3055
+
3056
+ for local_src, remote_dst in local_dirs:
3057
+ safe_src = _copy_tree_filtered(local_src)
3058
+ image = image.add_local_dir(safe_src, remote_dst)
3059
+
3060
+ secrets = {secret_names!r}
3061
+ secret_objs = [Secret.from_name(name) for name in secrets]
3062
+
3063
+ if INLINE_SECRET_VALUES:
3064
+ secret_objs.append(Secret.from_dict(INLINE_SECRET_VALUES))
3065
+
3066
+ if DOTENV_PATHS:
3067
+ secret_objs.extend(Secret.from_dotenv(path) for path in DOTENV_PATHS)
3068
+
3069
+ volume_mounts = {volume_mounts!r}
3070
+ volume_map = {{}}
3071
+ for vol_name, mount_path in volume_mounts:
3072
+ volume_map[mount_path] = Volume.from_name(vol_name, create_if_missing=True)
3073
+
3074
+ app = App(MODAL_APP_NAME)
3075
+
3076
+ @app.function(
3077
+ image=image,
3078
+ timeout={modal_cfg.timeout},
3079
+ memory={modal_cfg.memory},
3080
+ cpu={modal_cfg.cpu},
3081
+ min_containers={modal_cfg.min_containers},
3082
+ max_containers={modal_cfg.max_containers},
3083
+ secrets=secret_objs,
3084
+ volumes=volume_map,
3085
+ )
3086
+ @asgi_app()
3087
+ def fastapi_app():
3088
+ # Import the module to trigger registration (inside container)
3089
+ import os
3090
+ # Prefer mounted source over any preinstalled site-packages version
3091
+ import sys as _sys
3092
+ for k in list(_sys.modules.keys()):
3093
+ if k == 'synth_ai' or k.startswith('synth_ai.'):
3094
+ _sys.modules.pop(k, None)
3095
+ import importlib as _importlib
3096
+ _importlib.invalidate_caches()
3097
+ try:
3098
+ if MODULE_FILE and os.path.exists(MODULE_FILE):
3099
+ spec = importlib.util.spec_from_file_location(MODULE_NAME or 'task_app_module', MODULE_FILE)
3100
+ if not spec or not spec.loader:
3101
+ raise RuntimeError("Failed to prepare spec for: " + str(MODULE_FILE))
3102
+ mod = importlib.util.module_from_spec(spec)
3103
+ sys.modules[MODULE_NAME or 'task_app_module'] = mod
3104
+ spec.loader.exec_module(mod)
3105
+ else:
3106
+ try:
3107
+ importlib.import_module(MODULE_NAME)
3108
+ except Exception:
3109
+ fallback_file = '/opt/synth_ai_repo/__local_task_app__/' + (MODULE_NAME.split('.')[-1] if MODULE_NAME else 'task_app') + '.py'
3110
+ if os.path.exists(fallback_file):
3111
+ spec = importlib.util.spec_from_file_location(MODULE_NAME or 'task_app_module', fallback_file)
3112
+ if not spec or not spec.loader:
3113
+ raise RuntimeError("Failed to prepare fallback spec for: " + str(fallback_file))
3114
+ mod = importlib.util.module_from_spec(spec)
3115
+ sys.modules[MODULE_NAME or 'task_app_module'] = mod
3116
+ spec.loader.exec_module(mod)
3117
+ else:
3118
+ raise
3119
+ except Exception as e:
3120
+ raise RuntimeError("Task app import failed: " + str(e))
3121
+
3122
+ # Get the entry from registry (now that it's registered)
3123
+ from synth_ai.sdk.task.apps import registry
3124
+ from synth_ai.sdk.task.server import create_task_app
3125
+ entry = registry.get(ENTRY_ID)
3126
+ cfg = entry.modal
3127
+ if cfg is None:
3128
+ raise RuntimeError("Modal configuration missing for task app " + ENTRY_ID)
3129
+ config = entry.config_factory()
3130
+ return create_task_app(config)
3131
+ """
3132
+
3133
+ with tempfile.NamedTemporaryFile("w", suffix=f"_{entry.app_id}_modal.py", delete=False) as tmp:
3134
+ tmp.write(script)
3135
+ tmp.flush()
3136
+ name = tmp.name
3137
+ return Path(name)
3138
+
3139
+
3140
+ def register(cli: click.Group) -> None:
3141
+ cli.add_command(serve_command)
3142
+ cli.add_command(task_app_group)
3143
+ cli.add_command(eval_command)
3144
+ cli.add_command(filter_command)
3145
+
3146
+
3147
+ eval_command = eval_core.command
3148
+
3149
+ filter_command = filter_core.command
3150
+
3151
+
3152
+ def register_eval(cli: click.Group) -> None:
3153
+ cli.add_command(eval_command)