synth-ai 0.2.6.dev1__py3-none-any.whl → 0.4.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (738) hide show
  1. synth_ai/__init__.py +44 -24
  2. synth_ai/__main__.py +30 -3
  3. synth_ai/cli/__init__.py +103 -48
  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/demo/__init__.py +3 -0
  25. synth_ai/cli/commands/demo/core.py +153 -0
  26. synth_ai/cli/commands/eval/__init__.py +10 -0
  27. synth_ai/cli/commands/eval/config.py +338 -0
  28. synth_ai/cli/commands/eval/core.py +256 -0
  29. synth_ai/cli/commands/eval/runner.py +704 -0
  30. synth_ai/cli/commands/eval/validation.py +60 -0
  31. synth_ai/cli/commands/filter/__init__.py +12 -0
  32. synth_ai/cli/commands/filter/core.py +424 -0
  33. synth_ai/cli/commands/filter/errors.py +55 -0
  34. synth_ai/cli/commands/filter/validation.py +77 -0
  35. synth_ai/cli/commands/help/__init__.py +185 -0
  36. synth_ai/cli/commands/help/core.py +72 -0
  37. synth_ai/cli/commands/scan/__init__.py +19 -0
  38. synth_ai/cli/commands/scan/cloudflare_scanner.py +403 -0
  39. synth_ai/cli/commands/scan/core.py +344 -0
  40. synth_ai/cli/commands/scan/health_checker.py +242 -0
  41. synth_ai/cli/commands/scan/local_scanner.py +278 -0
  42. synth_ai/cli/commands/scan/models.py +83 -0
  43. synth_ai/cli/commands/smoke/__init__.py +7 -0
  44. synth_ai/cli/commands/smoke/core.py +1428 -0
  45. synth_ai/cli/commands/status/__init__.py +3 -0
  46. synth_ai/cli/commands/status/client.py +91 -0
  47. synth_ai/cli/commands/status/config.py +12 -0
  48. synth_ai/cli/commands/status/errors.py +11 -0
  49. synth_ai/cli/commands/status/subcommands/__init__.py +3 -0
  50. synth_ai/cli/commands/status/subcommands/config.py +13 -0
  51. synth_ai/cli/commands/status/subcommands/files.py +34 -0
  52. synth_ai/cli/commands/status/subcommands/jobs.py +51 -0
  53. synth_ai/cli/commands/status/subcommands/models.py +35 -0
  54. synth_ai/cli/commands/status/subcommands/runs.py +34 -0
  55. synth_ai/cli/commands/status/subcommands/session.py +77 -0
  56. synth_ai/cli/commands/status/subcommands/summary.py +39 -0
  57. synth_ai/cli/commands/status/subcommands/utils.py +41 -0
  58. synth_ai/cli/commands/status/utils.py +23 -0
  59. synth_ai/cli/commands/train/__init__.py +53 -0
  60. synth_ai/cli/commands/train/core.py +22 -0
  61. synth_ai/cli/commands/train/errors.py +117 -0
  62. synth_ai/cli/commands/train/judge_schemas.py +201 -0
  63. synth_ai/cli/commands/train/judge_validation.py +305 -0
  64. synth_ai/cli/commands/train/prompt_learning_validation.py +633 -0
  65. synth_ai/cli/commands/train/validation.py +392 -0
  66. synth_ai/cli/demo_apps/__init__.py +10 -0
  67. synth_ai/cli/demo_apps/core/__init__.py +28 -0
  68. synth_ai/cli/demo_apps/core/cli.py +1735 -0
  69. synth_ai/cli/demo_apps/crafter/__init__.py +1 -0
  70. synth_ai/cli/demo_apps/crafter/crafter_fft_4b.toml +55 -0
  71. synth_ai/cli/demo_apps/crafter/grpo_crafter_task_app.py +186 -0
  72. synth_ai/cli/demo_apps/crafter/rl_from_base_qwen4b.toml +74 -0
  73. synth_ai/cli/demo_apps/demo_registry.py +176 -0
  74. synth_ai/cli/demo_apps/demo_task_apps/__init__.py +7 -0
  75. synth_ai/{demos → cli/demo_apps}/demo_task_apps/core.py +117 -51
  76. synth_ai/cli/demo_apps/demo_task_apps/crafter/__init__.py +1 -0
  77. synth_ai/cli/demo_apps/demo_task_apps/crafter/configs/crafter_fft_4b.toml +53 -0
  78. synth_ai/cli/demo_apps/demo_task_apps/crafter/configs/rl_from_base_qwen4b.toml +73 -0
  79. synth_ai/cli/demo_apps/demo_task_apps/crafter/grpo_crafter_task_app.py +185 -0
  80. synth_ai/cli/demo_apps/demo_task_apps/math/_common.py +16 -0
  81. synth_ai/{demos → cli/demo_apps}/demo_task_apps/math/app.py +2 -1
  82. synth_ai/cli/demo_apps/demo_task_apps/math/config.toml +73 -0
  83. synth_ai/{demos → cli/demo_apps}/demo_task_apps/math/deploy_modal.py +3 -6
  84. synth_ai/cli/demo_apps/demo_task_apps/math/modal_task_app.py +738 -0
  85. synth_ai/cli/demo_apps/demo_task_apps/math/task_app_entry.py +39 -0
  86. synth_ai/cli/demo_apps/math/__init__.py +1 -0
  87. synth_ai/cli/demo_apps/math/_common.py +16 -0
  88. synth_ai/cli/demo_apps/math/app.py +38 -0
  89. synth_ai/cli/demo_apps/math/config.toml +75 -0
  90. synth_ai/cli/demo_apps/math/deploy_modal.py +54 -0
  91. synth_ai/cli/demo_apps/math/modal_task_app.py +698 -0
  92. synth_ai/cli/demo_apps/math/task_app_entry.py +53 -0
  93. synth_ai/cli/demo_apps/mipro/main.py +271 -0
  94. synth_ai/cli/demo_apps/mipro/task_app.py +922 -0
  95. synth_ai/cli/demo_apps/mipro/train_cfg.toml +92 -0
  96. synth_ai/cli/demos/__init__.py +12 -0
  97. synth_ai/cli/demos/demo.py +32 -0
  98. synth_ai/cli/demos/rl_demo.py +254 -0
  99. synth_ai/cli/deploy.py +216 -0
  100. synth_ai/cli/infra/__init__.py +14 -0
  101. synth_ai/cli/{balance.py → infra/balance.py} +21 -3
  102. synth_ai/cli/infra/mcp.py +35 -0
  103. synth_ai/cli/infra/modal_app.py +36 -0
  104. synth_ai/cli/infra/setup.py +69 -0
  105. synth_ai/cli/infra/status.py +16 -0
  106. synth_ai/cli/infra/turso.py +77 -0
  107. synth_ai/cli/lib/__init__.py +10 -0
  108. synth_ai/cli/lib/agents.py +76 -0
  109. synth_ai/cli/lib/apps/modal_app.py +101 -0
  110. synth_ai/cli/lib/apps/task_app.py +642 -0
  111. synth_ai/cli/lib/bin.py +39 -0
  112. synth_ai/cli/lib/env.py +375 -0
  113. synth_ai/cli/lib/errors.py +85 -0
  114. synth_ai/cli/lib/modal.py +315 -0
  115. synth_ai/cli/lib/plotting.py +126 -0
  116. synth_ai/cli/lib/prompt_args.py +39 -0
  117. synth_ai/cli/lib/prompts.py +284 -0
  118. synth_ai/cli/lib/sqld.py +122 -0
  119. synth_ai/cli/lib/task_app_discovery.py +884 -0
  120. synth_ai/cli/lib/task_app_env.py +295 -0
  121. synth_ai/cli/lib/train_cfgs.py +300 -0
  122. synth_ai/cli/lib/tunnel_records.py +207 -0
  123. synth_ai/cli/local/__init__.py +14 -0
  124. synth_ai/cli/local/experiment_queue/__init__.py +72 -0
  125. synth_ai/cli/local/experiment_queue/api_schemas.py +221 -0
  126. synth_ai/cli/local/experiment_queue/celery_app.py +208 -0
  127. synth_ai/cli/local/experiment_queue/config.py +128 -0
  128. synth_ai/cli/local/experiment_queue/config_utils.py +272 -0
  129. synth_ai/cli/local/experiment_queue/database.py +175 -0
  130. synth_ai/cli/local/experiment_queue/dispatcher.py +119 -0
  131. synth_ai/cli/local/experiment_queue/models.py +231 -0
  132. synth_ai/cli/local/experiment_queue/progress_info.py +160 -0
  133. synth_ai/cli/local/experiment_queue/results.py +373 -0
  134. synth_ai/cli/local/experiment_queue/schemas.py +131 -0
  135. synth_ai/cli/local/experiment_queue/service.py +344 -0
  136. synth_ai/cli/local/experiment_queue/status.py +372 -0
  137. synth_ai/cli/local/experiment_queue/status_tracker.py +360 -0
  138. synth_ai/cli/local/experiment_queue/tasks.py +1984 -0
  139. synth_ai/cli/local/experiment_queue/trace_storage.py +65 -0
  140. synth_ai/cli/local/experiment_queue/validation.py +157 -0
  141. synth_ai/cli/local/session/__init__.py +92 -0
  142. synth_ai/cli/local/session/client.py +383 -0
  143. synth_ai/cli/local/session/constants.py +63 -0
  144. synth_ai/cli/local/session/exceptions.py +105 -0
  145. synth_ai/cli/local/session/manager.py +139 -0
  146. synth_ai/cli/local/session/models.py +89 -0
  147. synth_ai/cli/local/session/query.py +110 -0
  148. synth_ai/cli/root.py +150 -102
  149. synth_ai/cli/task_apps/__init__.py +37 -0
  150. synth_ai/cli/task_apps/commands.py +3145 -0
  151. synth_ai/cli/task_apps/deploy.py +7 -0
  152. synth_ai/cli/task_apps/list.py +26 -0
  153. synth_ai/cli/task_apps/main.py +36 -0
  154. synth_ai/cli/task_apps/modal_serve.py +11 -0
  155. synth_ai/cli/task_apps/serve.py +11 -0
  156. synth_ai/cli/training/__init__.py +8 -0
  157. synth_ai/cli/training/train.py +5 -0
  158. synth_ai/cli/training/train_cfg.py +34 -0
  159. synth_ai/cli/{watch.py → training/watch.py} +13 -18
  160. synth_ai/cli/turso.py +52 -0
  161. synth_ai/cli/utils/__init__.py +8 -0
  162. synth_ai/cli/utils/experiments.py +235 -0
  163. synth_ai/cli/utils/queue.py +504 -0
  164. synth_ai/cli/{recent.py → utils/recent.py} +13 -7
  165. synth_ai/cli/{traces.py → utils/traces.py} +9 -5
  166. synth_ai/contracts/__init__.py +67 -0
  167. synth_ai/core/__init__.py +100 -0
  168. synth_ai/core/_utils/__init__.py +54 -0
  169. synth_ai/core/_utils/base_url.py +10 -0
  170. synth_ai/core/_utils/http.py +10 -0
  171. synth_ai/core/_utils/prompts.py +14 -0
  172. synth_ai/core/_utils/task_app_state.py +12 -0
  173. synth_ai/core/_utils/user_config.py +10 -0
  174. synth_ai/core/apps/common.py +116 -0
  175. synth_ai/core/auth.py +95 -0
  176. synth_ai/core/cfgs.py +240 -0
  177. synth_ai/core/config/__init__.py +16 -0
  178. synth_ai/core/config/base.py +168 -0
  179. synth_ai/core/config/resolver.py +89 -0
  180. synth_ai/core/env.py +231 -0
  181. synth_ai/core/errors.py +126 -0
  182. synth_ai/core/http.py +230 -0
  183. synth_ai/core/integrations/__init__.py +11 -0
  184. synth_ai/core/integrations/cloudflare.py +1710 -0
  185. synth_ai/core/integrations/mcp/__init__.py +6 -0
  186. synth_ai/core/integrations/mcp/__main__.py +8 -0
  187. synth_ai/core/integrations/mcp/claude.py +36 -0
  188. synth_ai/core/integrations/mcp/main.py +254 -0
  189. synth_ai/core/integrations/mcp/setup.py +100 -0
  190. synth_ai/core/integrations/modal.py +277 -0
  191. synth_ai/core/json.py +72 -0
  192. synth_ai/core/log_filter.py +99 -0
  193. synth_ai/core/logging.py +82 -0
  194. synth_ai/core/paths.py +107 -0
  195. synth_ai/core/pricing.py +109 -0
  196. synth_ai/core/process.py +233 -0
  197. synth_ai/core/ssl.py +25 -0
  198. synth_ai/core/storage/__init__.py +71 -0
  199. synth_ai/core/task_app_state.py +318 -0
  200. synth_ai/core/telemetry.py +282 -0
  201. synth_ai/{tracing_v3 → core/tracing_v3}/__init__.py +5 -1
  202. synth_ai/{tracing_v3 → core/tracing_v3}/abstractions.py +21 -4
  203. synth_ai/core/tracing_v3/config.py +229 -0
  204. synth_ai/core/tracing_v3/constants.py +21 -0
  205. synth_ai/{tracing_v3 → core/tracing_v3}/db_config.py +42 -29
  206. synth_ai/{tracing_v3 → core/tracing_v3}/decorators.py +80 -45
  207. synth_ai/{tracing_v3 → core/tracing_v3}/examples/basic_usage.py +15 -9
  208. synth_ai/{tracing_v3 → core/tracing_v3}/hooks.py +6 -4
  209. synth_ai/{tracing_v3 → core/tracing_v3}/llm_call_record_helpers.py +161 -61
  210. synth_ai/{tracing_v3 → core/tracing_v3}/migration_helper.py +1 -2
  211. synth_ai/{tracing_v3 → core/tracing_v3}/replica_sync.py +12 -7
  212. synth_ai/core/tracing_v3/serialization.py +130 -0
  213. synth_ai/{tracing_v3 → core/tracing_v3}/session_tracer.py +88 -21
  214. synth_ai/{tracing_v3 → core/tracing_v3}/storage/base.py +99 -12
  215. synth_ai/core/tracing_v3/storage/config.py +109 -0
  216. synth_ai/{tracing_v3 → core/tracing_v3}/storage/factory.py +11 -9
  217. synth_ai/{tracing_v3 → core/tracing_v3}/storage/utils.py +15 -11
  218. synth_ai/core/tracing_v3/trace_utils.py +326 -0
  219. synth_ai/core/tracing_v3/turso/__init__.py +12 -0
  220. synth_ai/core/tracing_v3/turso/daemon.py +278 -0
  221. synth_ai/{tracing_v3 → core/tracing_v3}/turso/models.py +7 -3
  222. synth_ai/core/tracing_v3/turso/native_manager.py +1385 -0
  223. synth_ai/{tracing_v3 → core/tracing_v3}/utils.py +5 -4
  224. synth_ai/core/urls.py +18 -0
  225. synth_ai/core/user_config.py +137 -0
  226. synth_ai/core/uvicorn.py +222 -0
  227. synth_ai/data/__init__.py +83 -0
  228. synth_ai/data/enums.py +123 -0
  229. synth_ai/data/rewards.py +152 -0
  230. synth_ai/data/traces.py +35 -0
  231. synth_ai/products/__init__.py +6 -0
  232. synth_ai/products/graph_evolve/__init__.py +46 -0
  233. synth_ai/products/graph_evolve/client.py +226 -0
  234. synth_ai/products/graph_evolve/config.py +591 -0
  235. synth_ai/products/graph_evolve/converters/__init__.py +42 -0
  236. synth_ai/products/graph_evolve/converters/openai_sft.py +484 -0
  237. synth_ai/products/graph_evolve/examples/hotpotqa/config.toml +109 -0
  238. synth_ai/products/graph_evolve/run.py +222 -0
  239. synth_ai/products/graph_gepa/__init__.py +23 -0
  240. synth_ai/products/graph_gepa/converters/__init__.py +19 -0
  241. synth_ai/products/graph_gepa/converters/openai_sft.py +29 -0
  242. synth_ai/sdk/__init__.py +123 -0
  243. synth_ai/sdk/api/__init__.py +1 -0
  244. synth_ai/sdk/api/models/supported.py +514 -0
  245. synth_ai/sdk/api/research_agent/__init__.py +296 -0
  246. synth_ai/sdk/api/train/__init__.py +85 -0
  247. synth_ai/sdk/api/train/builders.py +895 -0
  248. synth_ai/sdk/api/train/cli.py +2199 -0
  249. synth_ai/sdk/api/train/config_finder.py +267 -0
  250. synth_ai/sdk/api/train/configs/__init__.py +65 -0
  251. synth_ai/sdk/api/train/configs/prompt_learning.py +1706 -0
  252. synth_ai/sdk/api/train/configs/rl.py +187 -0
  253. synth_ai/sdk/api/train/configs/sft.py +99 -0
  254. synth_ai/sdk/api/train/configs/shared.py +81 -0
  255. synth_ai/sdk/api/train/context_learning.py +312 -0
  256. synth_ai/sdk/api/train/env_resolver.py +418 -0
  257. synth_ai/sdk/api/train/graph_validators.py +216 -0
  258. synth_ai/sdk/api/train/graphgen.py +984 -0
  259. synth_ai/sdk/api/train/graphgen_models.py +823 -0
  260. synth_ai/sdk/api/train/graphgen_validators.py +109 -0
  261. synth_ai/sdk/api/train/local_api.py +10 -0
  262. synth_ai/sdk/api/train/pollers.py +124 -0
  263. synth_ai/sdk/api/train/progress/__init__.py +97 -0
  264. synth_ai/sdk/api/train/progress/dataclasses.py +569 -0
  265. synth_ai/sdk/api/train/progress/events.py +326 -0
  266. synth_ai/sdk/api/train/progress/results.py +428 -0
  267. synth_ai/sdk/api/train/progress/tracker.py +641 -0
  268. synth_ai/sdk/api/train/prompt_learning.py +469 -0
  269. synth_ai/sdk/api/train/rl.py +441 -0
  270. synth_ai/sdk/api/train/sft.py +396 -0
  271. synth_ai/sdk/api/train/summary.py +522 -0
  272. synth_ai/sdk/api/train/supported_algos.py +147 -0
  273. synth_ai/sdk/api/train/task_app.py +351 -0
  274. synth_ai/sdk/api/train/utils.py +279 -0
  275. synth_ai/sdk/api/train/validators.py +2424 -0
  276. synth_ai/sdk/graphs/__init__.py +15 -0
  277. synth_ai/sdk/graphs/completions.py +570 -0
  278. synth_ai/{inference → sdk/inference}/__init__.py +0 -1
  279. synth_ai/sdk/inference/client.py +128 -0
  280. synth_ai/sdk/jobs/__init__.py +16 -0
  281. synth_ai/sdk/jobs/client.py +371 -0
  282. synth_ai/sdk/judging/__init__.py +14 -0
  283. synth_ai/sdk/judging/base.py +24 -0
  284. synth_ai/sdk/judging/client.py +40 -0
  285. synth_ai/sdk/judging/schemas.py +222 -0
  286. synth_ai/sdk/judging/types.py +42 -0
  287. synth_ai/sdk/learning/__init__.py +99 -0
  288. synth_ai/sdk/learning/algorithms.py +14 -0
  289. synth_ai/{learning → sdk/learning}/client.py +121 -30
  290. synth_ai/sdk/learning/config.py +5 -0
  291. synth_ai/{learning → sdk/learning}/constants.py +0 -2
  292. synth_ai/sdk/learning/context_learning_client.py +531 -0
  293. synth_ai/sdk/learning/context_learning_types.py +292 -0
  294. synth_ai/sdk/learning/ft_client.py +7 -0
  295. synth_ai/{learning → sdk/learning}/health.py +15 -9
  296. synth_ai/{learning → sdk/learning}/jobs.py +44 -47
  297. synth_ai/sdk/learning/prompt_extraction.py +334 -0
  298. synth_ai/sdk/learning/prompt_learning_client.py +455 -0
  299. synth_ai/sdk/learning/prompt_learning_types.py +186 -0
  300. synth_ai/{rl → sdk/learning/rl}/__init__.py +13 -8
  301. synth_ai/{learning/rl_client.py → sdk/learning/rl/client.py} +89 -77
  302. synth_ai/sdk/learning/rl/config.py +31 -0
  303. synth_ai/{rl → sdk/learning/rl}/contracts.py +5 -14
  304. synth_ai/{rl → sdk/learning/rl}/env_keys.py +45 -16
  305. synth_ai/sdk/learning/rl/secrets.py +13 -0
  306. synth_ai/sdk/learning/rl_client.py +5 -0
  307. synth_ai/sdk/learning/sft/__init__.py +29 -0
  308. synth_ai/sdk/learning/sft/client.py +95 -0
  309. synth_ai/sdk/learning/sft/config.py +270 -0
  310. synth_ai/sdk/learning/sft/data.py +698 -0
  311. synth_ai/sdk/learning/sse.py +57 -0
  312. synth_ai/sdk/learning/validators.py +52 -0
  313. synth_ai/sdk/localapi/__init__.py +40 -0
  314. synth_ai/sdk/localapi/apps/__init__.py +28 -0
  315. synth_ai/sdk/localapi/client.py +10 -0
  316. synth_ai/sdk/localapi/contracts.py +10 -0
  317. synth_ai/sdk/localapi/helpers.py +519 -0
  318. synth_ai/sdk/localapi/rollouts.py +87 -0
  319. synth_ai/sdk/localapi/server.py +29 -0
  320. synth_ai/sdk/localapi/template.py +70 -0
  321. synth_ai/sdk/streaming/__init__.py +35 -0
  322. synth_ai/sdk/streaming/config.py +94 -0
  323. synth_ai/sdk/streaming/handlers.py +1997 -0
  324. synth_ai/sdk/streaming/streamer.py +713 -0
  325. synth_ai/sdk/streaming/types.py +112 -0
  326. synth_ai/sdk/task/__init__.py +164 -0
  327. synth_ai/sdk/task/apps/__init__.py +169 -0
  328. synth_ai/sdk/task/auth.py +165 -0
  329. synth_ai/sdk/task/client.py +175 -0
  330. synth_ai/sdk/task/config.py +257 -0
  331. synth_ai/sdk/task/contracts.py +219 -0
  332. synth_ai/sdk/task/datasets.py +108 -0
  333. synth_ai/sdk/task/errors.py +50 -0
  334. synth_ai/sdk/task/health.py +34 -0
  335. synth_ai/sdk/task/in_process.py +1190 -0
  336. synth_ai/sdk/task/in_process_runner.py +314 -0
  337. synth_ai/sdk/task/inference_api.py +299 -0
  338. synth_ai/sdk/task/json.py +111 -0
  339. synth_ai/sdk/task/proxy.py +287 -0
  340. synth_ai/sdk/task/rubrics/__init__.py +55 -0
  341. synth_ai/sdk/task/rubrics/loaders.py +156 -0
  342. synth_ai/sdk/task/rubrics/models.py +57 -0
  343. synth_ai/sdk/task/rubrics/scoring.py +116 -0
  344. synth_ai/sdk/task/rubrics/strict.py +149 -0
  345. synth_ai/sdk/task/rubrics.py +219 -0
  346. synth_ai/sdk/task/server.py +631 -0
  347. synth_ai/sdk/task/trace_correlation_helpers.py +539 -0
  348. synth_ai/sdk/task/tracing_utils.py +95 -0
  349. synth_ai/sdk/task/validators.py +441 -0
  350. synth_ai/sdk/task/vendors.py +59 -0
  351. synth_ai/sdk/training/__init__.py +102 -0
  352. synth_ai/sdk/tunnels/__init__.py +83 -0
  353. synth_ai/sdk/tunnels/cleanup.py +83 -0
  354. synth_ai/sdk/tunnels/ports.py +120 -0
  355. synth_ai/utils/__init__.py +213 -0
  356. synth_ai-0.4.3.dist-info/METADATA +262 -0
  357. synth_ai-0.4.3.dist-info/RECORD +370 -0
  358. {synth_ai-0.2.6.dev1.dist-info → synth_ai-0.4.3.dist-info}/entry_points.txt +0 -1
  359. synth_ai/cli/calc.py +0 -69
  360. synth_ai/cli/demo.py +0 -131
  361. synth_ai/cli/legacy_root_backup.py +0 -470
  362. synth_ai/cli/man.py +0 -106
  363. synth_ai/cli/rl_demo.py +0 -137
  364. synth_ai/cli/status.py +0 -133
  365. synth_ai/config/base_url.py +0 -98
  366. synth_ai/core/experiment.py +0 -15
  367. synth_ai/core/system.py +0 -15
  368. synth_ai/demos/core/__init__.py +0 -1
  369. synth_ai/demos/core/cli.py +0 -685
  370. synth_ai/demos/demo_task_apps/__init__.py +0 -1
  371. synth_ai/demos/demo_task_apps/math/config.toml +0 -44
  372. synth_ai/demos/demo_task_apps/math/deploy_task_app.sh +0 -22
  373. synth_ai/environments/__init__.py +0 -31
  374. synth_ai/environments/environment/__init__.py +0 -1
  375. synth_ai/environments/environment/artifacts/__init__.py +0 -1
  376. synth_ai/environments/environment/artifacts/base.py +0 -52
  377. synth_ai/environments/environment/core.py +0 -67
  378. synth_ai/environments/environment/db/__init__.py +0 -1
  379. synth_ai/environments/environment/db/sqlite.py +0 -45
  380. synth_ai/environments/environment/registry.py +0 -233
  381. synth_ai/environments/environment/resources/sqlite.py +0 -45
  382. synth_ai/environments/environment/results.py +0 -1
  383. synth_ai/environments/environment/rewards/__init__.py +0 -1
  384. synth_ai/environments/environment/rewards/core.py +0 -29
  385. synth_ai/environments/environment/shared_engine.py +0 -26
  386. synth_ai/environments/environment/tools/__init__.py +0 -200
  387. synth_ai/environments/examples/__init__.py +0 -1
  388. synth_ai/environments/examples/bandit/__init__.py +0 -33
  389. synth_ai/environments/examples/bandit/engine.py +0 -294
  390. synth_ai/environments/examples/bandit/environment.py +0 -194
  391. synth_ai/environments/examples/bandit/taskset.py +0 -200
  392. synth_ai/environments/examples/crafter_classic/__init__.py +0 -8
  393. synth_ai/environments/examples/crafter_classic/agent_demos/analyze_semantic_words_markdown.py +0 -250
  394. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_comprehensive_evaluation.py +0 -59
  395. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_evaluation_browser.py +0 -152
  396. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_evaluation_config.toml +0 -24
  397. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_evaluation_framework.py +0 -1194
  398. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/crafter_synth_config.toml +0 -56
  399. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/filter_config_modal.toml +0 -32
  400. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/filter_traces_sft_turso.py +0 -724
  401. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/kick_off_ft_modal.py +0 -384
  402. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_action_results.py +0 -53
  403. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_agent_actions.py +0 -178
  404. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_latest_run.py +0 -222
  405. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_lm_traces.py +0 -183
  406. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_no_rewards.py +0 -210
  407. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/analyze_trace_issue.py +0 -206
  408. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/check_db_schema.py +0 -49
  409. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/check_latest_results.py +0 -64
  410. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/debug_agent_responses.py +0 -88
  411. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/old/quick_trace_check.py +0 -77
  412. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/compare_experiments.py +0 -324
  413. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/filter_traces_sft_turso.py +0 -580
  414. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/kick_off_ft_oai.py +0 -362
  415. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/multi_model_config.toml +0 -49
  416. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/analyze_enhanced_hooks.py +0 -332
  417. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/analyze_hook_events.py +0 -97
  418. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/analyze_hook_results.py +0 -217
  419. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/check_hook_storage.py +0 -87
  420. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/check_seeds.py +0 -88
  421. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/compare_seed_performance.py +0 -195
  422. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/custom_eval_pipelines.py +0 -400
  423. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/plot_hook_frequency.py +0 -195
  424. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/old/seed_analysis_summary.py +0 -56
  425. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/run_rollouts_for_models_and_compare_v3.py +0 -858
  426. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_quick_evaluation.py +0 -52
  427. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_react_agent.py +0 -874
  428. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_trace_evaluation.py +0 -1412
  429. synth_ai/environments/examples/crafter_classic/agent_demos/example_v3_usage.py +0 -216
  430. synth_ai/environments/examples/crafter_classic/agent_demos/old/compare_traces.py +0 -296
  431. synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_comprehensive_evaluation.py +0 -58
  432. synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_env_serialization.py +0 -464
  433. synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_evaluation_browser.py +0 -152
  434. synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_quick_evaluation.py +0 -51
  435. synth_ai/environments/examples/crafter_classic/agent_demos/old/crafter_trace_evaluation.py +0 -1412
  436. synth_ai/environments/examples/crafter_classic/agent_demos/old/debug_player_loss.py +0 -112
  437. synth_ai/environments/examples/crafter_classic/agent_demos/old/diagnose_service.py +0 -203
  438. synth_ai/environments/examples/crafter_classic/agent_demos/old/diagnose_slowness.py +0 -305
  439. synth_ai/environments/examples/crafter_classic/agent_demos/old/eval_by_difficulty.py +0 -126
  440. synth_ai/environments/examples/crafter_classic/agent_demos/old/eval_example.py +0 -94
  441. synth_ai/environments/examples/crafter_classic/agent_demos/old/explore_saved_states.py +0 -142
  442. synth_ai/environments/examples/crafter_classic/agent_demos/old/filter_traces_sft.py +0 -26
  443. synth_ai/environments/examples/crafter_classic/agent_demos/old/filter_traces_sft_OLD.py +0 -984
  444. synth_ai/environments/examples/crafter_classic/agent_demos/old/generate_ft_data_gemini.py +0 -724
  445. synth_ai/environments/examples/crafter_classic/agent_demos/old/generate_ft_data_modal.py +0 -386
  446. synth_ai/environments/examples/crafter_classic/agent_demos/old/generate_ft_metadata.py +0 -205
  447. synth_ai/environments/examples/crafter_classic/agent_demos/old/kick_off_ft_gemini.py +0 -150
  448. synth_ai/environments/examples/crafter_classic/agent_demos/old/kick_off_ft_modal.py +0 -283
  449. synth_ai/environments/examples/crafter_classic/agent_demos/old/prepare_vertex_ft.py +0 -280
  450. synth_ai/environments/examples/crafter_classic/agent_demos/old/profile_env_slowness.py +0 -456
  451. synth_ai/environments/examples/crafter_classic/agent_demos/old/replicate_issue.py +0 -166
  452. synth_ai/environments/examples/crafter_classic/agent_demos/old/run_and_eval.py +0 -102
  453. synth_ai/environments/examples/crafter_classic/agent_demos/old/run_comparison.py +0 -128
  454. synth_ai/environments/examples/crafter_classic/agent_demos/old/run_qwen_rollouts.py +0 -655
  455. synth_ai/environments/examples/crafter_classic/agent_demos/old/trace_eval_OLD.py +0 -202
  456. synth_ai/environments/examples/crafter_classic/agent_demos/old/validate_openai_format.py +0 -166
  457. synth_ai/environments/examples/crafter_classic/config_logging.py +0 -111
  458. synth_ai/environments/examples/crafter_classic/debug_translation.py +0 -0
  459. synth_ai/environments/examples/crafter_classic/engine.py +0 -579
  460. synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +0 -64
  461. synth_ai/environments/examples/crafter_classic/engine_helpers/action_map.py +0 -6
  462. synth_ai/environments/examples/crafter_classic/engine_helpers/serialization.py +0 -75
  463. synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +0 -267
  464. synth_ai/environments/examples/crafter_classic/environment.py +0 -404
  465. synth_ai/environments/examples/crafter_classic/taskset.py +0 -233
  466. synth_ai/environments/examples/crafter_classic/trace_hooks_v3.py +0 -228
  467. synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +0 -299
  468. synth_ai/environments/examples/crafter_custom/__init__.py +0 -4
  469. synth_ai/environments/examples/crafter_custom/agent_demos/__init__.py +0 -1
  470. synth_ai/environments/examples/crafter_custom/agent_demos/trace_eval.py +0 -202
  471. synth_ai/environments/examples/crafter_custom/crafter/__init__.py +0 -7
  472. synth_ai/environments/examples/crafter_custom/crafter/config.py +0 -182
  473. synth_ai/environments/examples/crafter_custom/crafter/constants.py +0 -8
  474. synth_ai/environments/examples/crafter_custom/crafter/engine.py +0 -269
  475. synth_ai/environments/examples/crafter_custom/crafter/env.py +0 -262
  476. synth_ai/environments/examples/crafter_custom/crafter/objects.py +0 -417
  477. synth_ai/environments/examples/crafter_custom/crafter/recorder.py +0 -187
  478. synth_ai/environments/examples/crafter_custom/crafter/worldgen.py +0 -118
  479. synth_ai/environments/examples/crafter_custom/dataset_builder.py +0 -373
  480. synth_ai/environments/examples/crafter_custom/environment.py +0 -312
  481. synth_ai/environments/examples/crafter_custom/old/analyze_diamond_issue.py +0 -159
  482. synth_ai/environments/examples/crafter_custom/old/analyze_diamond_spawning.py +0 -158
  483. synth_ai/environments/examples/crafter_custom/old/compare_worlds.py +0 -71
  484. synth_ai/environments/examples/crafter_custom/old/dataset_stats.py +0 -105
  485. synth_ai/environments/examples/crafter_custom/old/diamond_spawning_summary.py +0 -119
  486. synth_ai/environments/examples/crafter_custom/old/example_dataset_usage.py +0 -52
  487. synth_ai/environments/examples/crafter_custom/run_dataset.py +0 -305
  488. synth_ai/environments/examples/enron/art_helpers/email_search_tools.py +0 -156
  489. synth_ai/environments/examples/enron/art_helpers/local_email_db.py +0 -281
  490. synth_ai/environments/examples/enron/art_helpers/types_enron.py +0 -25
  491. synth_ai/environments/examples/enron/engine.py +0 -295
  492. synth_ai/environments/examples/enron/environment.py +0 -166
  493. synth_ai/environments/examples/enron/taskset.py +0 -112
  494. synth_ai/environments/examples/enron/units/keyword_stats.py +0 -112
  495. synth_ai/environments/examples/minigrid/__init__.py +0 -48
  496. synth_ai/environments/examples/minigrid/agent_demos/minigrid_evaluation_framework.py +0 -1188
  497. synth_ai/environments/examples/minigrid/agent_demos/minigrid_quick_evaluation.py +0 -48
  498. synth_ai/environments/examples/minigrid/agent_demos/minigrid_react_agent.py +0 -562
  499. synth_ai/environments/examples/minigrid/agent_demos/minigrid_trace_evaluation.py +0 -221
  500. synth_ai/environments/examples/minigrid/engine.py +0 -589
  501. synth_ai/environments/examples/minigrid/environment.py +0 -274
  502. synth_ai/environments/examples/minigrid/environment_mapping.py +0 -242
  503. synth_ai/environments/examples/minigrid/puzzle_loader.py +0 -417
  504. synth_ai/environments/examples/minigrid/taskset.py +0 -583
  505. synth_ai/environments/examples/nethack/__init__.py +0 -7
  506. synth_ai/environments/examples/nethack/achievements.py +0 -337
  507. synth_ai/environments/examples/nethack/agent_demos/nethack_evaluation_framework.py +0 -981
  508. synth_ai/environments/examples/nethack/agent_demos/nethack_quick_evaluation.py +0 -74
  509. synth_ai/environments/examples/nethack/agent_demos/nethack_react_agent.py +0 -831
  510. synth_ai/environments/examples/nethack/engine.py +0 -739
  511. synth_ai/environments/examples/nethack/environment.py +0 -256
  512. synth_ai/environments/examples/nethack/helpers/__init__.py +0 -41
  513. synth_ai/environments/examples/nethack/helpers/action_mapping.py +0 -301
  514. synth_ai/environments/examples/nethack/helpers/nle_wrapper.py +0 -402
  515. synth_ai/environments/examples/nethack/helpers/observation_utils.py +0 -433
  516. synth_ai/environments/examples/nethack/helpers/recording_wrapper.py +0 -200
  517. synth_ai/environments/examples/nethack/helpers/trajectory_recorder.py +0 -269
  518. synth_ai/environments/examples/nethack/helpers/visualization/replay_viewer.py +0 -308
  519. synth_ai/environments/examples/nethack/helpers/visualization/visualizer.py +0 -431
  520. synth_ai/environments/examples/nethack/taskset.py +0 -323
  521. synth_ai/environments/examples/red/__init__.py +0 -7
  522. synth_ai/environments/examples/red/agent_demos/__init__.py +0 -1
  523. synth_ai/environments/examples/red/config_logging.py +0 -110
  524. synth_ai/environments/examples/red/engine.py +0 -694
  525. synth_ai/environments/examples/red/engine_helpers/__init__.py +0 -1
  526. synth_ai/environments/examples/red/engine_helpers/memory_map.py +0 -28
  527. synth_ai/environments/examples/red/engine_helpers/reward_components.py +0 -276
  528. synth_ai/environments/examples/red/engine_helpers/reward_library/__init__.py +0 -142
  529. synth_ai/environments/examples/red/engine_helpers/reward_library/adaptive_rewards.py +0 -57
  530. synth_ai/environments/examples/red/engine_helpers/reward_library/battle_rewards.py +0 -284
  531. synth_ai/environments/examples/red/engine_helpers/reward_library/composite_rewards.py +0 -150
  532. synth_ai/environments/examples/red/engine_helpers/reward_library/economy_rewards.py +0 -138
  533. synth_ai/environments/examples/red/engine_helpers/reward_library/efficiency_rewards.py +0 -57
  534. synth_ai/environments/examples/red/engine_helpers/reward_library/exploration_rewards.py +0 -331
  535. synth_ai/environments/examples/red/engine_helpers/reward_library/novelty_rewards.py +0 -121
  536. synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_rewards.py +0 -559
  537. synth_ai/environments/examples/red/engine_helpers/reward_library/pokemon_rewards.py +0 -313
  538. synth_ai/environments/examples/red/engine_helpers/reward_library/social_rewards.py +0 -148
  539. synth_ai/environments/examples/red/engine_helpers/reward_library/story_rewards.py +0 -247
  540. synth_ai/environments/examples/red/engine_helpers/screen_analysis.py +0 -368
  541. synth_ai/environments/examples/red/engine_helpers/state_extraction.py +0 -140
  542. synth_ai/environments/examples/red/environment.py +0 -238
  543. synth_ai/environments/examples/red/taskset.py +0 -79
  544. synth_ai/environments/examples/red/units/__init__.py +0 -1
  545. synth_ai/environments/examples/sokoban/__init__.py +0 -1
  546. synth_ai/environments/examples/sokoban/agent_demos/sokoban_full_eval.py +0 -899
  547. synth_ai/environments/examples/sokoban/engine.py +0 -678
  548. synth_ai/environments/examples/sokoban/engine_helpers/__init__.py +0 -1
  549. synth_ai/environments/examples/sokoban/engine_helpers/room_utils.py +0 -657
  550. synth_ai/environments/examples/sokoban/engine_helpers/vendored/__init__.py +0 -18
  551. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/__init__.py +0 -3
  552. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/boxoban_env.py +0 -131
  553. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/render_utils.py +0 -370
  554. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/room_utils.py +0 -332
  555. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env.py +0 -306
  556. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_fixed_targets.py +0 -67
  557. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_pull.py +0 -115
  558. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_two_player.py +0 -123
  559. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_variations.py +0 -394
  560. synth_ai/environments/examples/sokoban/environment.py +0 -229
  561. synth_ai/environments/examples/sokoban/generate_verified_puzzles.py +0 -440
  562. synth_ai/environments/examples/sokoban/puzzle_loader.py +0 -312
  563. synth_ai/environments/examples/sokoban/taskset.py +0 -428
  564. synth_ai/environments/examples/sokoban/units/astar_common.py +0 -95
  565. synth_ai/environments/examples/tictactoe/__init__.py +0 -1
  566. synth_ai/environments/examples/tictactoe/engine.py +0 -368
  567. synth_ai/environments/examples/tictactoe/environment.py +0 -240
  568. synth_ai/environments/examples/tictactoe/taskset.py +0 -215
  569. synth_ai/environments/examples/verilog/__init__.py +0 -10
  570. synth_ai/environments/examples/verilog/engine.py +0 -329
  571. synth_ai/environments/examples/verilog/environment.py +0 -350
  572. synth_ai/environments/examples/verilog/taskset.py +0 -420
  573. synth_ai/environments/examples/wordle/__init__.py +0 -29
  574. synth_ai/environments/examples/wordle/engine.py +0 -398
  575. synth_ai/environments/examples/wordle/environment.py +0 -159
  576. synth_ai/environments/examples/wordle/helpers/generate_instances_wordfreq.py +0 -75
  577. synth_ai/environments/examples/wordle/taskset.py +0 -230
  578. synth_ai/environments/reproducibility/core.py +0 -42
  579. synth_ai/environments/reproducibility/helpers.py +0 -0
  580. synth_ai/environments/reproducibility/tree.py +0 -364
  581. synth_ai/environments/service/app.py +0 -91
  582. synth_ai/environments/service/core_routes.py +0 -1020
  583. synth_ai/environments/service/external_registry.py +0 -56
  584. synth_ai/environments/service/registry.py +0 -9
  585. synth_ai/environments/stateful/__init__.py +0 -1
  586. synth_ai/environments/stateful/core.py +0 -163
  587. synth_ai/environments/stateful/engine.py +0 -21
  588. synth_ai/environments/stateful/state.py +0 -7
  589. synth_ai/environments/tasks/api.py +0 -19
  590. synth_ai/environments/tasks/core.py +0 -80
  591. synth_ai/environments/tasks/filters.py +0 -41
  592. synth_ai/environments/tasks/utils.py +0 -91
  593. synth_ai/environments/v0_observability/history.py +0 -3
  594. synth_ai/environments/v0_observability/log.py +0 -2
  595. synth_ai/evals/base.py +0 -15
  596. synth_ai/experimental/synth_oss.py +0 -446
  597. synth_ai/http.py +0 -102
  598. synth_ai/inference/client.py +0 -20
  599. synth_ai/install_sqld.sh +0 -40
  600. synth_ai/jobs/client.py +0 -246
  601. synth_ai/learning/__init__.py +0 -24
  602. synth_ai/learning/config.py +0 -43
  603. synth_ai/learning/filtering.py +0 -0
  604. synth_ai/learning/ft_client.py +0 -59
  605. synth_ai/learning/offline/dpo.py +0 -0
  606. synth_ai/learning/offline/providers.py +0 -7
  607. synth_ai/learning/offline/sft.py +0 -0
  608. synth_ai/learning/offline/shared.py +0 -0
  609. synth_ai/learning/online/grpo.py +0 -0
  610. synth_ai/learning/online/irft.py +0 -0
  611. synth_ai/learning/prompts/banking77_injection_eval.py +0 -168
  612. synth_ai/learning/prompts/gepa.py +0 -0
  613. synth_ai/learning/prompts/hello_world_in_context_injection_ex.py +0 -213
  614. synth_ai/learning/prompts/mipro.py +0 -289
  615. synth_ai/learning/prompts/random_search.py +0 -246
  616. synth_ai/learning/prompts/run_mipro_banking77.py +0 -172
  617. synth_ai/learning/prompts/run_random_search_banking77.py +0 -324
  618. synth_ai/learning/sse.py +0 -58
  619. synth_ai/learning/validators.py +0 -48
  620. synth_ai/lm/__init__.py +0 -51
  621. synth_ai/lm/caching/constants.py +0 -6
  622. synth_ai/lm/caching/dbs.py +0 -0
  623. synth_ai/lm/caching/ephemeral.py +0 -102
  624. synth_ai/lm/caching/handler.py +0 -137
  625. synth_ai/lm/caching/initialize.py +0 -11
  626. synth_ai/lm/caching/persistent.py +0 -114
  627. synth_ai/lm/config.py +0 -110
  628. synth_ai/lm/constants.py +0 -32
  629. synth_ai/lm/core/__init__.py +0 -8
  630. synth_ai/lm/core/all.py +0 -73
  631. synth_ai/lm/core/exceptions.py +0 -7
  632. synth_ai/lm/core/main.py +0 -319
  633. synth_ai/lm/core/main_v3.py +0 -594
  634. synth_ai/lm/core/synth_models.py +0 -48
  635. synth_ai/lm/core/vendor_clients.py +0 -188
  636. synth_ai/lm/cost/__init__.py +0 -0
  637. synth_ai/lm/cost/monitor.py +0 -1
  638. synth_ai/lm/cost/statefulness.py +0 -1
  639. synth_ai/lm/injection.py +0 -80
  640. synth_ai/lm/overrides.py +0 -206
  641. synth_ai/lm/provider_support/__init__.py +0 -8
  642. synth_ai/lm/provider_support/anthropic.py +0 -972
  643. synth_ai/lm/provider_support/openai.py +0 -1139
  644. synth_ai/lm/provider_support/suppress_logging.py +0 -31
  645. synth_ai/lm/structured_outputs/__init__.py +0 -0
  646. synth_ai/lm/structured_outputs/handler.py +0 -440
  647. synth_ai/lm/structured_outputs/inject.py +0 -297
  648. synth_ai/lm/structured_outputs/rehabilitate.py +0 -185
  649. synth_ai/lm/tools/__init__.py +0 -3
  650. synth_ai/lm/tools/base.py +0 -172
  651. synth_ai/lm/unified_interface.py +0 -202
  652. synth_ai/lm/vendors/__init__.py +0 -0
  653. synth_ai/lm/vendors/base.py +0 -81
  654. synth_ai/lm/vendors/core/__init__.py +0 -0
  655. synth_ai/lm/vendors/core/anthropic_api.py +0 -387
  656. synth_ai/lm/vendors/core/gemini_api.py +0 -292
  657. synth_ai/lm/vendors/core/mistral_api.py +0 -322
  658. synth_ai/lm/vendors/core/openai_api.py +0 -220
  659. synth_ai/lm/vendors/core/synth_dev_api.py +0 -0
  660. synth_ai/lm/vendors/local/__init__.py +0 -0
  661. synth_ai/lm/vendors/local/ollama.py +0 -0
  662. synth_ai/lm/vendors/openai_standard.py +0 -780
  663. synth_ai/lm/vendors/openai_standard_responses.py +0 -256
  664. synth_ai/lm/vendors/retries.py +0 -22
  665. synth_ai/lm/vendors/supported/__init__.py +0 -0
  666. synth_ai/lm/vendors/supported/custom_endpoint.py +0 -417
  667. synth_ai/lm/vendors/supported/deepseek.py +0 -69
  668. synth_ai/lm/vendors/supported/grok.py +0 -75
  669. synth_ai/lm/vendors/supported/groq.py +0 -16
  670. synth_ai/lm/vendors/supported/ollama.py +0 -15
  671. synth_ai/lm/vendors/supported/openrouter.py +0 -74
  672. synth_ai/lm/vendors/supported/together.py +0 -11
  673. synth_ai/lm/vendors/synth_client.py +0 -808
  674. synth_ai/lm/warmup.py +0 -186
  675. synth_ai/rl/secrets.py +0 -19
  676. synth_ai/scripts/verify_rewards.py +0 -100
  677. synth_ai/task/__init__.py +0 -10
  678. synth_ai/task/contracts.py +0 -120
  679. synth_ai/task/health.py +0 -28
  680. synth_ai/task/validators.py +0 -12
  681. synth_ai/tracing/__init__.py +0 -30
  682. synth_ai/tracing_v1/__init__.py +0 -33
  683. synth_ai/tracing_v3/config.py +0 -84
  684. synth_ai/tracing_v3/storage/config.py +0 -62
  685. synth_ai/tracing_v3/turso/__init__.py +0 -25
  686. synth_ai/tracing_v3/turso/daemon.py +0 -144
  687. synth_ai/tracing_v3/turso/manager.py +0 -760
  688. synth_ai/v0/tracing/__init__.py +0 -0
  689. synth_ai/v0/tracing/abstractions.py +0 -224
  690. synth_ai/v0/tracing/base_client.py +0 -91
  691. synth_ai/v0/tracing/client_manager.py +0 -131
  692. synth_ai/v0/tracing/config.py +0 -140
  693. synth_ai/v0/tracing/context.py +0 -146
  694. synth_ai/v0/tracing/decorators.py +0 -680
  695. synth_ai/v0/tracing/events/__init__.py +0 -0
  696. synth_ai/v0/tracing/events/manage.py +0 -147
  697. synth_ai/v0/tracing/events/scope.py +0 -86
  698. synth_ai/v0/tracing/events/store.py +0 -228
  699. synth_ai/v0/tracing/immediate_client.py +0 -151
  700. synth_ai/v0/tracing/local.py +0 -18
  701. synth_ai/v0/tracing/log_client_base.py +0 -73
  702. synth_ai/v0/tracing/retry_queue.py +0 -186
  703. synth_ai/v0/tracing/trackers.py +0 -515
  704. synth_ai/v0/tracing/upload.py +0 -510
  705. synth_ai/v0/tracing/utils.py +0 -9
  706. synth_ai/v0/tracing_v1/__init__.py +0 -16
  707. synth_ai/v0/tracing_v1/abstractions.py +0 -224
  708. synth_ai/v0/tracing_v1/base_client.py +0 -91
  709. synth_ai/v0/tracing_v1/client_manager.py +0 -131
  710. synth_ai/v0/tracing_v1/config.py +0 -140
  711. synth_ai/v0/tracing_v1/context.py +0 -146
  712. synth_ai/v0/tracing_v1/decorators.py +0 -701
  713. synth_ai/v0/tracing_v1/events/__init__.py +0 -0
  714. synth_ai/v0/tracing_v1/events/manage.py +0 -147
  715. synth_ai/v0/tracing_v1/events/scope.py +0 -86
  716. synth_ai/v0/tracing_v1/events/store.py +0 -228
  717. synth_ai/v0/tracing_v1/immediate_client.py +0 -151
  718. synth_ai/v0/tracing_v1/local.py +0 -18
  719. synth_ai/v0/tracing_v1/log_client_base.py +0 -73
  720. synth_ai/v0/tracing_v1/retry_queue.py +0 -186
  721. synth_ai/v0/tracing_v1/trackers.py +0 -515
  722. synth_ai/v0/tracing_v1/upload.py +0 -525
  723. synth_ai/v0/tracing_v1/utils.py +0 -9
  724. synth_ai/zyk/__init__.py +0 -30
  725. synth_ai-0.2.6.dev1.dist-info/METADATA +0 -106
  726. synth_ai-0.2.6.dev1.dist-info/RECORD +0 -416
  727. /synth_ai/{demos → cli/demo_apps}/demo_task_apps/math/__init__.py +0 -0
  728. /synth_ai/{lm/caching → core/apps}/__init__.py +0 -0
  729. /synth_ai/{tracing_v3 → core/tracing_v3}/lm_call_record_abstractions.py +0 -0
  730. /synth_ai/{tracing_v3 → core/tracing_v3}/storage/__init__.py +0 -0
  731. /synth_ai/{tracing_v3 → core/tracing_v3}/storage/exceptions.py +0 -0
  732. /synth_ai/{tracing_v3 → core/tracing_v3}/storage/types.py +0 -0
  733. /synth_ai/{compound/cais.py → py.typed} +0 -0
  734. /synth_ai/{learning → sdk/learning}/core.py +0 -0
  735. /synth_ai/{learning → sdk/learning}/gateway.py +0 -0
  736. {synth_ai-0.2.6.dev1.dist-info → synth_ai-0.4.3.dist-info}/WHEEL +0 -0
  737. {synth_ai-0.2.6.dev1.dist-info → synth_ai-0.4.3.dist-info}/licenses/LICENSE +0 -0
  738. {synth_ai-0.2.6.dev1.dist-info → synth_ai-0.4.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1190 @@
1
+ """In-process task app support for local development and demos."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ import os
8
+ import signal
9
+ import socket
10
+ import subprocess
11
+ import threading
12
+ from pathlib import Path
13
+ from typing import Any, Callable, Optional
14
+ from urllib.parse import urlparse
15
+
16
+ import httpx
17
+ import uvicorn
18
+ from uvicorn._types import ASGIApplication
19
+
20
+ from synth_ai.core.apps.common import get_asgi_app, load_module
21
+ from synth_ai.core.integrations.cloudflare import (
22
+ create_tunnel,
23
+ ensure_cloudflared_installed,
24
+ open_managed_tunnel,
25
+ open_quick_tunnel_with_dns_verification,
26
+ rotate_tunnel,
27
+ stop_tunnel,
28
+ wait_for_health_check,
29
+ )
30
+ from synth_ai.core.paths import REPO_ROOT, configure_import_paths
31
+ from synth_ai.sdk.task.server import TaskAppConfig, create_task_app
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ # Global registry for signal handlers
36
+ _registered_instances: set[InProcessTaskApp] = set()
37
+
38
+
39
+ def _find_available_port(host: str, start_port: int, max_attempts: int = 100) -> int:
40
+ """
41
+ Find an available port starting from start_port.
42
+
43
+ Args:
44
+ host: Host to bind to
45
+ start_port: Starting port number
46
+ max_attempts: Maximum number of ports to try
47
+
48
+ Returns:
49
+ Available port number
50
+
51
+ Raises:
52
+ RuntimeError: If no available port found
53
+ """
54
+ for offset in range(max_attempts):
55
+ port = start_port + offset
56
+ try:
57
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
58
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
59
+ sock.bind((host, port))
60
+ return port
61
+ except OSError:
62
+ continue
63
+
64
+ raise RuntimeError(
65
+ f"Could not find available port starting from {start_port} "
66
+ f"(tried {max_attempts} ports)"
67
+ )
68
+
69
+
70
+ def _is_port_available(host: str, port: int) -> bool:
71
+ """Check if a port is available."""
72
+ try:
73
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
74
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
75
+ sock.bind((host, port))
76
+ return True
77
+ except OSError:
78
+ return False
79
+
80
+
81
+
82
+
83
+ def _kill_process_on_port(host: str, port: int) -> None:
84
+ """
85
+ Attempt to kill any process using the specified port.
86
+
87
+ Note: This is a best-effort operation and may not work on all systems.
88
+ """
89
+ import subprocess
90
+ import sys
91
+
92
+ try:
93
+ if sys.platform == "win32":
94
+ # Windows
95
+ result = subprocess.run(
96
+ ["netstat", "-ano"], capture_output=True, text=True, check=False
97
+ )
98
+ for line in result.stdout.splitlines():
99
+ if f":{port}" in line and "LISTENING" in line:
100
+ parts = line.split()
101
+ if len(parts) > 4:
102
+ pid = parts[-1]
103
+ try:
104
+ subprocess.run(
105
+ ["taskkill", "/F", "/PID", pid],
106
+ capture_output=True,
107
+ check=False,
108
+ )
109
+ logger.info(f"Killed process {pid} on port {port}")
110
+ except Exception:
111
+ pass
112
+ else:
113
+ # Unix-like (macOS, Linux)
114
+ result = subprocess.run(
115
+ ["lsof", "-ti", f":{port}"],
116
+ capture_output=True,
117
+ text=True,
118
+ check=False,
119
+ )
120
+ if result.stdout.strip():
121
+ pids = result.stdout.strip().split()
122
+ for pid in pids:
123
+ try:
124
+ subprocess.run(
125
+ ["kill", "-9", pid],
126
+ capture_output=True,
127
+ check=False,
128
+ )
129
+ logger.info(f"Killed process {pid} on port {port}")
130
+ except Exception:
131
+ pass
132
+ except Exception as e:
133
+ logger.debug(f"Could not kill process on port {port}: {e}")
134
+
135
+
136
+ async def _resolve_via_public_dns(hostname: str, timeout: float = 5.0) -> Optional[str]:
137
+ """
138
+ Resolve hostname using public DNS servers (1.1.1.1, 8.8.8.8).
139
+
140
+ This bypasses local DNS caching issues that can cause NXDOMAIN errors
141
+ when the local resolver has stale cached responses.
142
+
143
+ Returns the first resolved IP address, or None if resolution fails.
144
+ """
145
+ loop = asyncio.get_event_loop()
146
+
147
+ for dns_server in ("1.1.1.1", "8.8.8.8"):
148
+ try:
149
+ result = await loop.run_in_executor(
150
+ None,
151
+ lambda server=dns_server: subprocess.run(
152
+ ["dig", f"@{server}", "+short", hostname],
153
+ capture_output=True,
154
+ text=True,
155
+ timeout=timeout,
156
+ ),
157
+ )
158
+ if result.returncode == 0 and result.stdout.strip():
159
+ # Return first IP (dig may return multiple)
160
+ first_ip = result.stdout.strip().splitlines()[0].strip()
161
+ if first_ip and not first_ip.endswith("."): # Skip CNAME responses
162
+ logger.debug(f"Resolved {hostname} via {dns_server}: {first_ip}")
163
+ return first_ip
164
+ except FileNotFoundError:
165
+ # dig not available, try socket resolution instead
166
+ try:
167
+ result = await loop.run_in_executor(
168
+ None,
169
+ lambda: socket.gethostbyname(hostname),
170
+ )
171
+ if result:
172
+ logger.debug(f"Resolved {hostname} via system DNS: {result}")
173
+ return result
174
+ except socket.gaierror:
175
+ pass
176
+ except subprocess.TimeoutExpired:
177
+ logger.debug(f"DNS timeout resolving {hostname} via {dns_server}")
178
+ except Exception as e:
179
+ logger.debug(f"DNS resolution error for {hostname} via {dns_server}: {e}")
180
+
181
+ return None
182
+
183
+
184
+ async def _verify_tunnel_ready(
185
+ tunnel_url: str,
186
+ api_key: str,
187
+ *,
188
+ max_retries: int | None = None,
189
+ retry_delay: float | None = None,
190
+ timeout_per_request: float = 10.0,
191
+ verify_tls: bool = True,
192
+ ) -> bool:
193
+ """
194
+ Verify that a Cloudflare tunnel is actually routing traffic (not just DNS-resolvable).
195
+
196
+ A tunnel can resolve via DNS before HTTP routing is ready. This helper polls both
197
+ /health and /task_info until they return 200 or retries are exhausted.
198
+
199
+ IMPORTANT: Uses public DNS (1.1.1.1) to bypass local DNS cache issues.
200
+ Local DNS may have stale NXDOMAIN cached even after tunnel DNS is created.
201
+
202
+ Environment variables for tuning:
203
+ SYNTH_TUNNEL_VERIFY_MAX_RETRIES: Number of retry attempts (default: 30)
204
+ SYNTH_TUNNEL_VERIFY_DELAY_SECS: Delay between retries in seconds (default: 2.0)
205
+
206
+ With defaults, waits up to 60 seconds for tunnel to become ready.
207
+ """
208
+ # Allow env var overrides for reliability tuning
209
+ if max_retries is None:
210
+ max_retries = int(os.getenv("SYNTH_TUNNEL_VERIFY_MAX_RETRIES", "30"))
211
+ if retry_delay is None:
212
+ retry_delay = float(os.getenv("SYNTH_TUNNEL_VERIFY_DELAY_SECS", "2.0"))
213
+
214
+ # Initial delay before first check - tunnels often need a moment after DNS resolves
215
+ initial_delay = float(os.getenv("SYNTH_TUNNEL_VERIFY_INITIAL_DELAY_SECS", "3.0"))
216
+ if initial_delay > 0:
217
+ logger.debug(f"Waiting {initial_delay}s for tunnel to stabilize before verification...")
218
+ await asyncio.sleep(initial_delay)
219
+
220
+ # Parse hostname from URL
221
+ parsed = urlparse(tunnel_url)
222
+ hostname = parsed.netloc
223
+ scheme = parsed.scheme or "https"
224
+
225
+ headers = {
226
+ "X-API-Key": api_key,
227
+ "Authorization": f"Bearer {api_key}",
228
+ "Host": hostname, # Always set Host header for IP-based requests
229
+ }
230
+ aliases = (os.getenv("ENVIRONMENT_API_KEY_ALIASES") or "").strip()
231
+ if aliases:
232
+ headers["X-API-Keys"] = ",".join(
233
+ [api_key, *[p.strip() for p in aliases.split(",") if p.strip()]]
234
+ )
235
+
236
+ logger.info(f"Verifying tunnel is routing traffic (max {max_retries} attempts, {retry_delay}s delay)...")
237
+
238
+ # Track resolved IP to avoid re-resolving every attempt
239
+ resolved_ip: Optional[str] = None
240
+
241
+ for attempt in range(max_retries):
242
+ try:
243
+ # Try to resolve IP via public DNS if we don't have it yet
244
+ if resolved_ip is None:
245
+ resolved_ip = await _resolve_via_public_dns(hostname)
246
+ if resolved_ip:
247
+ logger.info(f"Resolved tunnel hostname via public DNS: {hostname} -> {resolved_ip}")
248
+
249
+ # If we have a resolved IP, use curl with --resolve for proper SNI handling
250
+ # httpx connecting to an IP directly fails SSL handshake due to SNI issues
251
+ if resolved_ip:
252
+ loop = asyncio.get_event_loop()
253
+
254
+ # Use curl with --resolve to bypass local DNS while maintaining proper SNI
255
+ async def curl_check(path: str) -> int:
256
+ try:
257
+ result = await loop.run_in_executor(
258
+ None,
259
+ lambda: subprocess.run(
260
+ [
261
+ "curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
262
+ "--resolve", f"{hostname}:443:{resolved_ip}",
263
+ "-H", f"X-API-Key: {api_key}",
264
+ "-H", f"Authorization: Bearer {api_key}",
265
+ f"https://{hostname}{path}",
266
+ ],
267
+ capture_output=True,
268
+ text=True,
269
+ timeout=timeout_per_request,
270
+ ),
271
+ )
272
+ return int(result.stdout.strip()) if result.returncode == 0 else 0
273
+ except Exception:
274
+ return 0
275
+
276
+ health_status = await curl_check("/health")
277
+ task_info_status = await curl_check("/task_info")
278
+
279
+ if health_status == 200 and task_info_status == 200:
280
+ logger.info(
281
+ f"Tunnel ready after {attempt + 1} attempt(s): "
282
+ f"health={health_status}, task_info={task_info_status}"
283
+ )
284
+ return True
285
+
286
+ logger.debug(
287
+ "Tunnel not ready (attempt %s/%s): health=%s task_info=%s",
288
+ attempt + 1,
289
+ max_retries,
290
+ health_status,
291
+ task_info_status,
292
+ )
293
+ else:
294
+ # Fall back to hostname-based request (local DNS)
295
+ base = tunnel_url.rstrip("/")
296
+ async with httpx.AsyncClient(timeout=timeout_per_request, verify=verify_tls) as client:
297
+ health = await client.get(f"{base}/health", headers=headers)
298
+ task_info = await client.get(f"{base}/task_info", headers=headers)
299
+
300
+ if health.status_code == 200 and task_info.status_code == 200:
301
+ logger.info(
302
+ f"Tunnel ready after {attempt + 1} attempt(s): "
303
+ f"health={health.status_code}, task_info={task_info.status_code}"
304
+ )
305
+ return True
306
+
307
+ logger.debug(
308
+ "Tunnel not ready (attempt %s/%s): health=%s task_info=%s",
309
+ attempt + 1,
310
+ max_retries,
311
+ health.status_code,
312
+ task_info.status_code,
313
+ )
314
+ except Exception as exc: # pragma: no cover - defensive
315
+ logger.debug(
316
+ "Tunnel readiness check failed (attempt %s/%s): %s",
317
+ attempt + 1,
318
+ max_retries,
319
+ exc,
320
+ )
321
+ # Clear resolved IP on connection errors - might need to re-resolve
322
+ if "connect" in str(exc).lower() or "resolve" in str(exc).lower():
323
+ resolved_ip = None
324
+
325
+ if attempt < max_retries - 1:
326
+ # Log progress periodically (every 5 attempts)
327
+ if (attempt + 1) % 5 == 0:
328
+ elapsed = (attempt + 1) * retry_delay
329
+ remaining = (max_retries - attempt - 1) * retry_delay
330
+ logger.info(
331
+ f"Still waiting for tunnel... ({elapsed:.0f}s elapsed, {remaining:.0f}s remaining)"
332
+ )
333
+ await asyncio.sleep(retry_delay)
334
+
335
+ logger.warning(f"Tunnel verification exhausted after {max_retries} attempts")
336
+ return False
337
+
338
+
339
+ async def _verify_preconfigured_url_ready(
340
+ tunnel_url: str,
341
+ api_key: str,
342
+ *,
343
+ extra_headers: Optional[dict[str, str]] = None,
344
+ max_retries: int = 10,
345
+ retry_delay: float = 1.0,
346
+ timeout_per_request: float = 10.0,
347
+ ) -> bool:
348
+ """
349
+ Verify that a preconfigured tunnel URL is routing traffic.
350
+
351
+ This is similar to _verify_tunnel_ready but designed for external tunnel
352
+ providers (ngrok, etc.) where we don't control the tunnel setup.
353
+
354
+ Args:
355
+ tunnel_url: The external tunnel URL to verify
356
+ api_key: API key for task app authentication
357
+ extra_headers: Additional headers for the tunnel (e.g., auth tokens)
358
+ max_retries: Number of retry attempts
359
+ retry_delay: Delay between retries in seconds
360
+ timeout_per_request: Timeout for each HTTP request
361
+
362
+ Returns:
363
+ True if tunnel is accessible, False otherwise
364
+ """
365
+ base = tunnel_url.rstrip("/")
366
+ headers = {
367
+ "X-API-Key": api_key,
368
+ "Authorization": f"Bearer {api_key}",
369
+ }
370
+
371
+ # Add any extra headers (e.g., custom auth tokens)
372
+ if extra_headers:
373
+ headers.update(extra_headers)
374
+
375
+ logger.info(f"Verifying preconfigured URL is accessible (max {max_retries} attempts)...")
376
+
377
+ for attempt in range(max_retries):
378
+ try:
379
+ async with httpx.AsyncClient(timeout=timeout_per_request) as client:
380
+ health = await client.get(f"{base}/health", headers=headers)
381
+
382
+ # Only accept 200 for health checks - other codes may indicate misrouting
383
+ if health.status_code == 200:
384
+ logger.info(
385
+ f"Preconfigured URL ready after {attempt + 1} attempt(s): "
386
+ f"health={health.status_code}"
387
+ )
388
+ return True
389
+
390
+ logger.debug(
391
+ "Preconfigured URL not ready (attempt %s/%s): health=%s",
392
+ attempt + 1,
393
+ max_retries,
394
+ health.status_code,
395
+ )
396
+ except Exception as exc:
397
+ logger.debug(
398
+ "Preconfigured URL check failed (attempt %s/%s): %s",
399
+ attempt + 1,
400
+ max_retries,
401
+ exc,
402
+ )
403
+
404
+ if attempt < max_retries - 1:
405
+ await asyncio.sleep(retry_delay)
406
+
407
+ logger.warning(f"Preconfigured URL verification exhausted after {max_retries} attempts")
408
+ return False
409
+
410
+
411
+ class InProcessTaskApp:
412
+ """
413
+ Context manager for running task apps in-process with automatic tunneling.
414
+
415
+ This class simplifies local development and demos by:
416
+ 1. Starting a task app server in a background thread
417
+ 2. Opening a tunnel automatically (Cloudflare by default, or use preconfigured URL)
418
+ 3. Providing the tunnel URL for GEPA/MIPRO jobs
419
+ 4. Cleaning up everything on exit
420
+
421
+ Supports multiple input methods:
422
+ - FastAPI app instance (most direct)
423
+ - TaskAppConfig object
424
+ - Config factory function (Callable[[], TaskAppConfig])
425
+ - Task app file path (fallback for compatibility)
426
+
427
+ Tunnel modes:
428
+ - "quick": Cloudflare quick tunnel (default for local dev)
429
+ - "named": Cloudflare named/managed tunnel
430
+ - "local": No tunnel, use localhost URL directly
431
+ - "preconfigured": Use externally-provided URL (set via preconfigured_url param or
432
+ SYNTH_TASK_APP_URL env var). Useful for ngrok or other external tunnel providers.
433
+
434
+ Example:
435
+ ```python
436
+ from synth_ai.sdk.task.in_process import InProcessTaskApp
437
+ from heartdisease_task_app import build_config
438
+
439
+ # Default: use Cloudflare quick tunnel
440
+ async with InProcessTaskApp(
441
+ config_factory=build_config,
442
+ port=8114,
443
+ ) as task_app:
444
+ print(f"Task app running at: {task_app.url}")
445
+
446
+ # Use preconfigured URL (e.g., from ngrok, localtunnel, etc.)
447
+ async with InProcessTaskApp(
448
+ config_factory=build_config,
449
+ port=8000,
450
+ tunnel_mode="preconfigured",
451
+ preconfigured_url="https://abc123.ngrok.io",
452
+ ) as task_app:
453
+ print(f"Task app running at: {task_app.url}")
454
+ ```
455
+ """
456
+
457
+ def __init__(
458
+ self,
459
+ *,
460
+ app: Optional[ASGIApplication] = None,
461
+ config: Optional[TaskAppConfig] = None,
462
+ config_factory: Optional[Callable[[], TaskAppConfig]] = None,
463
+ task_app_path: Optional[Path | str] = None,
464
+ port: int = 8114,
465
+ host: str = "127.0.0.1",
466
+ tunnel_mode: str = "quick",
467
+ preconfigured_url: Optional[str] = None,
468
+ preconfigured_auth_header: Optional[str] = None,
469
+ preconfigured_auth_token: Optional[str] = None,
470
+ api_key: Optional[str] = None,
471
+ health_check_timeout: float = 30.0,
472
+ auto_find_port: bool = True,
473
+ skip_tunnel_verification: bool = True, # Default True - verification is unreliable
474
+ force_new_tunnel: bool = False,
475
+ on_start: Optional[Callable[[InProcessTaskApp], None]] = None,
476
+ on_stop: Optional[Callable[[InProcessTaskApp], None]] = None,
477
+ ):
478
+ """
479
+ Initialize in-process task app.
480
+
481
+ Args:
482
+ app: FastAPI app instance (most direct)
483
+ config: TaskAppConfig object
484
+ config_factory: Callable that returns TaskAppConfig
485
+ task_app_path: Path to task app .py file (fallback)
486
+ port: Local port to run server on
487
+ host: Host to bind to (default: 127.0.0.1, use 0.0.0.0 for external access)
488
+ tunnel_mode: Tunnel mode - "quick", "named", "local", or "preconfigured"
489
+ preconfigured_url: External tunnel URL to use when tunnel_mode="preconfigured".
490
+ Can also be set via SYNTH_TASK_APP_URL env var.
491
+ preconfigured_auth_header: Optional auth header name for preconfigured URL
492
+ (e.g., "x-custom-auth-token")
493
+ preconfigured_auth_token: Optional auth token value for preconfigured URL
494
+ api_key: API key for health checks (defaults to ENVIRONMENT_API_KEY env var)
495
+ health_check_timeout: Max time to wait for health check in seconds
496
+ auto_find_port: If True, automatically find available port if requested port is busy
497
+ skip_tunnel_verification: If True, skip HTTP verification of tunnel connectivity.
498
+ Useful when the tunnel URL is known to be valid.
499
+ force_new_tunnel: If True, create a fresh tunnel instead of reusing existing one.
500
+ Use this when an existing managed tunnel is stale/broken.
501
+ on_start: Optional callback called when task app starts (receives self)
502
+ on_stop: Optional callback called when task app stops (receives self)
503
+
504
+ Raises:
505
+ ValueError: If multiple or no input methods provided, or invalid parameters
506
+ FileNotFoundError: If task_app_path doesn't exist
507
+ """
508
+ # Validate: exactly one input method
509
+ inputs = [app, config, config_factory, task_app_path]
510
+ if sum(x is not None for x in inputs) != 1:
511
+ raise ValueError(
512
+ "Must provide exactly one of: app, config, config_factory, or task_app_path"
513
+ )
514
+
515
+ # Validate port range
516
+ if not (1024 <= port <= 65535):
517
+ raise ValueError(f"Port must be in range [1024, 65535], got {port}")
518
+
519
+ # Validate host (allow 0.0.0.0 for container environments)
520
+ allowed_hosts = ("127.0.0.1", "localhost", "0.0.0.0")
521
+ if host not in allowed_hosts:
522
+ raise ValueError(
523
+ f"Host must be one of {allowed_hosts} for security reasons, got {host}"
524
+ )
525
+
526
+ # Validate tunnel_mode
527
+ valid_modes = ("local", "quick", "named", "preconfigured")
528
+ if tunnel_mode not in valid_modes:
529
+ raise ValueError(f"tunnel_mode must be one of {valid_modes}, got {tunnel_mode}")
530
+
531
+ # Validate task_app_path if provided
532
+ if task_app_path:
533
+ path = Path(task_app_path)
534
+ if not path.exists():
535
+ raise FileNotFoundError(f"Task app path does not exist: {task_app_path}")
536
+ if path.suffix != ".py":
537
+ raise ValueError(
538
+ f"Task app path must be a .py file, got {task_app_path}"
539
+ )
540
+
541
+ self._app_input = app
542
+ self._config = config
543
+ self._config_factory = config_factory
544
+ self._task_app_path = Path(task_app_path) if task_app_path else None
545
+
546
+ self.port = port
547
+ self.host = host
548
+ self.tunnel_mode = tunnel_mode
549
+ self.preconfigured_url = preconfigured_url
550
+ self.preconfigured_auth_header = preconfigured_auth_header
551
+ self.preconfigured_auth_token = preconfigured_auth_token
552
+ self.api_key = api_key
553
+ self.health_check_timeout = health_check_timeout
554
+ self.auto_find_port = auto_find_port
555
+ self.skip_tunnel_verification = skip_tunnel_verification
556
+ self.force_new_tunnel = force_new_tunnel
557
+ self.on_start = on_start
558
+ self.on_stop = on_stop
559
+
560
+ self.url: Optional[str] = None
561
+ self._tunnel_proc: Optional[Any] = None
562
+ self._app: Optional[ASGIApplication] = None
563
+ self._uvicorn_server: Optional[uvicorn.Server] = None
564
+ self._server_thread: Optional[Any] = None
565
+ self._original_port = port # Track original requested port
566
+ self._is_preconfigured = False # Track if using preconfigured URL
567
+ self._dns_verified_by_backend = False # Track if backend verified DNS propagation
568
+
569
+ async def __aenter__(self) -> InProcessTaskApp:
570
+ """Start task app and tunnel."""
571
+
572
+ # For named tunnels, pre-fetch tunnel config to get the correct port
573
+ # (existing tunnels are configured for a specific port)
574
+ mode = os.getenv("SYNTH_TUNNEL_MODE", self.tunnel_mode)
575
+ if mode == "named":
576
+ try:
577
+ from synth_ai.core.env import get_api_key as get_synth_api_key
578
+ synth_api_key = get_synth_api_key()
579
+ if synth_api_key is None:
580
+ raise ValueError("SYNTH_API_KEY is required for named tunnel mode")
581
+ tunnel_config = await self._fetch_tunnel_config(synth_api_key)
582
+ tunnel_port = tunnel_config.get("local_port")
583
+ if tunnel_config.get("hostname") and tunnel_port and tunnel_port != self.port:
584
+ logger.info(
585
+ f"Existing managed tunnel is configured for port {tunnel_port}, "
586
+ f"adjusting from requested port {self.port}"
587
+ )
588
+ self.port = tunnel_port
589
+ # Store config for later use to avoid re-fetching
590
+ self._prefetched_tunnel_config = tunnel_config
591
+ except Exception as e:
592
+ logger.debug(f"Pre-fetch tunnel config failed: {e}")
593
+ self._prefetched_tunnel_config = None
594
+ else:
595
+ self._prefetched_tunnel_config = None
596
+
597
+ logger.debug(f"Starting in-process task app on {self.host}:{self.port}")
598
+
599
+ # For named tunnels, the port is baked into the tunnel config - we MUST use it
600
+ tunnel_config = getattr(self, "_prefetched_tunnel_config", None) or {}
601
+ tunnel_port = tunnel_config.get("local_port")
602
+ is_named_tunnel_port = mode == "named" and tunnel_port and tunnel_port == self.port
603
+
604
+ # Handle port conflicts
605
+ if not _is_port_available(self.host, self.port):
606
+ if is_named_tunnel_port:
607
+ # Named tunnel port is REQUIRED - kill whatever is using it
608
+ print(f"[CLOUDFLARE-FIX] Named tunnel requires port {self.port}, killing existing process...")
609
+ logger.warning(
610
+ f"Named tunnel is configured for port {self.port}, killing existing process..."
611
+ )
612
+ _kill_process_on_port(self.host, self.port)
613
+ await asyncio.sleep(1.0) # Wait for port to free
614
+
615
+ if not _is_port_available(self.host, self.port):
616
+ raise RuntimeError(
617
+ f"Named tunnel requires port {self.port} but it's still in use after kill attempt. "
618
+ "Manually kill the process using this port, or delete and recreate the tunnel."
619
+ )
620
+ print(f"[CLOUDFLARE-FIX] Port {self.port} freed successfully")
621
+ elif self.auto_find_port:
622
+ print(f"Port {self.port} is in use, attempting to find available port...")
623
+ logger.warning(
624
+ f"Port {self.port} is in use, attempting to find available port..."
625
+ )
626
+ self.port = _find_available_port(self.host, self.port)
627
+ logger.debug(f"Using port {self.port} instead")
628
+ else:
629
+ # Try to kill process on port
630
+ logger.warning(
631
+ f"Port {self.port} is in use, attempting to free it..."
632
+ )
633
+ _kill_process_on_port(self.host, self.port)
634
+ await asyncio.sleep(0.5) # Brief wait for port to free
635
+
636
+ if not _is_port_available(self.host, self.port):
637
+ raise RuntimeError(
638
+ f"Port {self.port} is still in use. "
639
+ "Set auto_find_port=True to automatically find an available port."
640
+ )
641
+
642
+ # 1. Get FastAPI app from whichever input method was provided
643
+ if self._app_input:
644
+ # Direct app - use as-is
645
+ self._app = self._app_input
646
+
647
+ elif self._config:
648
+ # TaskAppConfig - create app from it
649
+ self._app = create_task_app(self._config) # type: ignore[assignment]
650
+
651
+ elif self._config_factory:
652
+ # Callable - call it to get config, then create app
653
+ config = self._config_factory()
654
+ self._app = create_task_app(config) # type: ignore[assignment]
655
+
656
+ elif self._task_app_path:
657
+ # File path - load module and extract app
658
+ configure_import_paths(self._task_app_path, REPO_ROOT)
659
+ module = load_module(
660
+ self._task_app_path,
661
+ f"_inprocess_{self._task_app_path.stem}_{id(self)}",
662
+ )
663
+
664
+ # Try to get app directly first
665
+ try:
666
+ self._app = get_asgi_app(module) # type: ignore[assignment]
667
+ except RuntimeError:
668
+ # If no app found, try to get build_config function
669
+ build_config = getattr(module, "build_config", None)
670
+ if build_config and callable(build_config):
671
+ config = build_config()
672
+ self._app = create_task_app(config) # type: ignore[assignment]
673
+ else:
674
+ # Try registry lookup as last resort
675
+ from synth_ai.sdk.task.apps import registry
676
+ app_id = getattr(module, "APP_ID", None) or self._task_app_path.stem
677
+ entry = registry.get(app_id)
678
+ if entry and entry.config_factory:
679
+ config = entry.config_factory()
680
+ self._app = create_task_app(config) # type: ignore[assignment]
681
+ else:
682
+ raise RuntimeError(
683
+ f"Task app at {self._task_app_path} must expose either:\n"
684
+ f" - An ASGI app via `app = FastAPI(...)` or factory function\n"
685
+ f" - A `build_config()` function that returns TaskAppConfig\n"
686
+ f" - Be registered with register_local_api() or register_task_app()"
687
+ ) from None
688
+
689
+ # 2. Start uvicorn in background thread
690
+ # Use daemon=True for local testing to allow quick exit
691
+ # The thread will be killed when the process exits
692
+ logger.debug(f"Starting uvicorn server on {self.host}:{self.port}")
693
+
694
+ config = uvicorn.Config(
695
+ self._app, # type: ignore[arg-type]
696
+ host=self.host,
697
+ port=self.port,
698
+ reload=False,
699
+ log_level="info",
700
+ )
701
+ self._uvicorn_server = uvicorn.Server(config)
702
+
703
+ def serve():
704
+ try:
705
+ self._uvicorn_server.run() # type: ignore[attr-defined]
706
+ except Exception as exc:
707
+ logger.debug(f"Uvicorn server stopped: {exc}")
708
+
709
+ self._server_thread = threading.Thread(
710
+ target=serve,
711
+ name=f"synth-uvicorn-{self.port}",
712
+ daemon=True, # Daemon thread dies when main process exits
713
+ )
714
+ self._server_thread.start()
715
+
716
+ # 3. Wait for health check
717
+ api_key = self.api_key or self._get_api_key()
718
+ logger.debug(f"Waiting for health check on {self.host}:{self.port}")
719
+ await wait_for_health_check(
720
+ self.host, self.port, api_key, timeout=self.health_check_timeout
721
+ )
722
+ logger.debug(f"Health check passed for {self.host}:{self.port}")
723
+
724
+ # 4. Determine tunnel mode (env var can override)
725
+ mode = os.getenv("SYNTH_TUNNEL_MODE", self.tunnel_mode)
726
+
727
+ # Check for preconfigured URL via env var
728
+ env_preconfigured_url = os.getenv("SYNTH_TASK_APP_URL")
729
+ if env_preconfigured_url:
730
+ mode = "preconfigured"
731
+ self.preconfigured_url = env_preconfigured_url
732
+ logger.info(f"Using preconfigured URL from SYNTH_TASK_APP_URL: {env_preconfigured_url}")
733
+
734
+ override_host = os.getenv("SYNTH_TUNNEL_HOSTNAME")
735
+
736
+ if mode == "preconfigured":
737
+ # Preconfigured mode: use externally-provided URL (e.g., ngrok, localtunnel)
738
+ # This bypasses Cloudflare entirely - the caller is responsible for the tunnel
739
+ if not self.preconfigured_url:
740
+ raise ValueError(
741
+ "tunnel_mode='preconfigured' requires preconfigured_url parameter "
742
+ "or SYNTH_TASK_APP_URL environment variable"
743
+ )
744
+
745
+ self.url = self.preconfigured_url.rstrip("/")
746
+ self._tunnel_proc = None
747
+ self._is_preconfigured = True
748
+ logger.info(f"Using preconfigured tunnel URL: {self.url}")
749
+
750
+ # Optionally verify the preconfigured URL is accessible
751
+ if not self.skip_tunnel_verification:
752
+ api_key = self.api_key or self._get_api_key()
753
+
754
+ # Build headers including any custom auth for the tunnel
755
+ extra_headers: dict[str, str] = {}
756
+ if self.preconfigured_auth_header and self.preconfigured_auth_token:
757
+ extra_headers[self.preconfigured_auth_header] = self.preconfigured_auth_token
758
+
759
+ ready = await _verify_preconfigured_url_ready(
760
+ self.url,
761
+ api_key,
762
+ extra_headers=extra_headers,
763
+ max_retries=10, # Fewer retries - external URL should work quickly
764
+ retry_delay=1.0,
765
+ )
766
+ if ready:
767
+ logger.info(f"Preconfigured URL verified and ready: {self.url}")
768
+ else:
769
+ logger.warning(
770
+ f"Preconfigured URL {self.url} may not be accessible. "
771
+ "Proceeding anyway - set skip_tunnel_verification=True to suppress this warning."
772
+ )
773
+ elif mode == "local":
774
+ # Local mode: skip tunnel, use localhost
775
+ self.url = f"http://{self.host}:{self.port}"
776
+ self._tunnel_proc = None
777
+ logger.debug(f"Using local mode: {self.url}")
778
+ elif mode == "named":
779
+ # Named tunnel mode: fully automatic managed tunnel
780
+ # 1. Check for existing tunnel
781
+ # 2. Auto-create if none exists
782
+ # 3. Auto-start cloudflared if not accessible
783
+ # 4. Verify tunnel is working
784
+ ensure_cloudflared_installed()
785
+
786
+ # For tunnel config, we need the SYNTH_API_KEY (not ENVIRONMENT_API_KEY)
787
+ from synth_ai.core.env import get_api_key as get_synth_api_key
788
+ synth_api_key = get_synth_api_key()
789
+ if synth_api_key is None:
790
+ raise ValueError("SYNTH_API_KEY is required for named tunnel mode")
791
+
792
+ # For task app auth, use the environment API key
793
+ api_key = self.api_key or self._get_api_key()
794
+
795
+ # Use pre-fetched config (port was already adjusted before server started)
796
+ tunnel_config = getattr(self, "_prefetched_tunnel_config", None) or {}
797
+ if not tunnel_config:
798
+ # Fetch if not pre-fetched (shouldn't happen normally)
799
+ tunnel_config = await self._fetch_tunnel_config(synth_api_key)
800
+
801
+ named_host = tunnel_config.get("hostname")
802
+ tunnel_token = tunnel_config.get("tunnel_token")
803
+
804
+ # Track if backend verified DNS (so we can skip local verification)
805
+ dns_verified_by_backend = False
806
+
807
+ # Force ROTATE tunnel if requested (deletes old + creates new, stays within limits)
808
+ if self.force_new_tunnel:
809
+ print("[CLOUDFLARE-FIX] force_new_tunnel=True, rotating tunnel...")
810
+ logger.info("force_new_tunnel=True, rotating tunnel (delete+create)")
811
+ try:
812
+ rotated = await rotate_tunnel(
813
+ synth_api_key=synth_api_key,
814
+ port=self.port,
815
+ reason="force_new_tunnel=True",
816
+ )
817
+ named_host = rotated.get("hostname")
818
+ tunnel_token = rotated.get("tunnel_token")
819
+ dns_verified_by_backend = rotated.get("dns_verified", False)
820
+ print(f"[CLOUDFLARE-FIX] Rotated to fresh tunnel: {named_host}")
821
+ print(f"[CLOUDFLARE-FIX] DNS verified by backend: {dns_verified_by_backend}")
822
+ logger.info(f"Rotated to fresh managed tunnel: {named_host}, dns_verified={dns_verified_by_backend}")
823
+ except Exception as e:
824
+ print(f"[CLOUDFLARE-FIX] Rotation failed: {e}, using existing tunnel: {named_host}")
825
+ logger.warning(f"Rotation failed: {e}, falling back to existing tunnel: {named_host}")
826
+ if not named_host or not tunnel_token:
827
+ raise RuntimeError(
828
+ f"Tunnel rotation failed and no existing tunnel found: {e}\n"
829
+ "Try using tunnel_mode='quick' instead."
830
+ ) from e
831
+ # Auto-create tunnel if none exists
832
+ elif not named_host:
833
+ logger.info("No managed tunnel found, creating one automatically...")
834
+ try:
835
+ # Generate subdomain from port or use default
836
+ subdomain = f"task-app-{self.port}"
837
+ new_tunnel = await create_tunnel(
838
+ synth_api_key=synth_api_key,
839
+ port=self.port,
840
+ subdomain=subdomain,
841
+ )
842
+ named_host = new_tunnel.get("hostname")
843
+ tunnel_token = new_tunnel.get("tunnel_token")
844
+ dns_verified_by_backend = new_tunnel.get("dns_verified", False)
845
+ logger.info(f"Created managed tunnel: {named_host}, dns_verified={dns_verified_by_backend}")
846
+ except Exception as e:
847
+ # If tunnel creation fails, suggest using quick tunnels
848
+ raise RuntimeError(
849
+ f"Failed to create managed tunnel: {e}\n"
850
+ "This may be because the backend doesn't have Cloudflare configured.\n"
851
+ "Options:\n"
852
+ " 1. Use tunnel_mode='quick' for automatic quick tunnels\n"
853
+ " 2. Ask your admin to configure Cloudflare credentials on the backend"
854
+ ) from e
855
+
856
+ if not named_host or not tunnel_token:
857
+ raise RuntimeError(
858
+ "Tunnel configuration incomplete (missing hostname or token). "
859
+ "Try deleting and recreating the tunnel, or use tunnel_mode='quick'."
860
+ )
861
+
862
+ self.url = f"https://{named_host}"
863
+ # Store dns_verified for use by job (to skip health check)
864
+ self._dns_verified_by_backend = dns_verified_by_backend
865
+
866
+ print(f"[CLOUDFLARE] Named tunnel URL: {self.url}")
867
+
868
+ # CRITICAL: For Cloudflare managed tunnels, DNS will NOT resolve until cloudflared connects.
869
+ # The DNS record exists in Cloudflare, but proxied CNAMEs to .cfargotunnel.com only
870
+ # resolve when the tunnel has an active cloudflared connection.
871
+ # Therefore, we MUST start cloudflared FIRST, then verify the tunnel works.
872
+
873
+ # First, check if cloudflared is already running (tunnel might be accessible)
874
+ ready = await _verify_tunnel_ready(
875
+ self.url,
876
+ api_key,
877
+ max_retries=1, # Single quick check
878
+ retry_delay=0.5,
879
+ verify_tls=_should_verify_tls(),
880
+ )
881
+
882
+ if ready:
883
+ # Tunnel already accessible - cloudflared must be running elsewhere
884
+ self._tunnel_proc = None
885
+ print(f"[CLOUDFLARE] Tunnel already accessible (cloudflared running externally)")
886
+ logger.info(f"Tunnel {self.url} is already accessible (cloudflared running externally)")
887
+ else:
888
+ # Tunnel not accessible - start cloudflared FIRST, then verify
889
+ print(f"[CLOUDFLARE] Starting cloudflared (DNS requires active tunnel connection)...")
890
+ logger.info(f"Starting cloudflared for {self.url}...")
891
+ try:
892
+ self._tunnel_proc = open_managed_tunnel(tunnel_token)
893
+ print(f"[CLOUDFLARE] cloudflared started, PID={self._tunnel_proc.pid}")
894
+ logger.info(f"Started cloudflared (PID: {self._tunnel_proc.pid})")
895
+ except Exception as e:
896
+ print(f"[CLOUDFLARE] ERROR starting cloudflared: {e}")
897
+ raise RuntimeError(
898
+ f"Failed to start cloudflared: {e}\n"
899
+ "Make sure cloudflared is installed: brew install cloudflare/cloudflare/cloudflared"
900
+ ) from e
901
+
902
+ # Wait for cloudflared to connect and tunnel to become accessible
903
+ print(f"[CLOUDFLARE] Waiting for tunnel to become accessible...")
904
+ ready = await _verify_tunnel_ready(
905
+ self.url,
906
+ api_key,
907
+ max_retries=15, # Up to ~30 seconds for tunnel to connect
908
+ retry_delay=2.0,
909
+ verify_tls=_should_verify_tls(),
910
+ )
911
+
912
+ if not ready:
913
+ # Tunnel still not accessible after starting cloudflared
914
+ # Clean up and try auto-rotation
915
+ if self._tunnel_proc:
916
+ stop_tunnel(self._tunnel_proc)
917
+ self._tunnel_proc = None
918
+
919
+ print(f"[CLOUDFLARE] Tunnel {self.url} not accessible, attempting rotation...")
920
+ logger.warning(f"Tunnel {self.url} failed to connect. Attempting rotation...")
921
+
922
+ try:
923
+ rotated = await rotate_tunnel(
924
+ synth_api_key=synth_api_key,
925
+ port=self.port,
926
+ reason=f"Tunnel {named_host} failed to connect",
927
+ )
928
+ named_host = rotated.get("hostname")
929
+ tunnel_token = rotated.get("tunnel_token")
930
+
931
+ if not named_host or not tunnel_token:
932
+ raise RuntimeError("Rotation returned incomplete tunnel config")
933
+
934
+ self.url = f"https://{named_host}"
935
+ print(f"[CLOUDFLARE] Rotated to new tunnel: {self.url}")
936
+
937
+ # Start cloudflared with the new token
938
+ self._tunnel_proc = open_managed_tunnel(tunnel_token)
939
+ print(f"[CLOUDFLARE] Started cloudflared for rotated tunnel, PID={self._tunnel_proc.pid}")
940
+
941
+ # Verify the new tunnel
942
+ ready = await _verify_tunnel_ready(
943
+ self.url,
944
+ api_key,
945
+ max_retries=15,
946
+ retry_delay=2.0,
947
+ verify_tls=_should_verify_tls(),
948
+ )
949
+
950
+ if not ready:
951
+ if self._tunnel_proc:
952
+ stop_tunnel(self._tunnel_proc)
953
+ self._tunnel_proc = None
954
+ raise RuntimeError(
955
+ f"Rotated tunnel {self.url} also failed. "
956
+ "Try using tunnel_mode='quick' instead."
957
+ )
958
+
959
+ print(f"[CLOUDFLARE] Rotated tunnel ready: {self.url}")
960
+
961
+ except Exception as rotate_err:
962
+ raise RuntimeError(
963
+ f"Tunnel failed and rotation failed: {rotate_err}\n"
964
+ "Try using tunnel_mode='quick' instead."
965
+ ) from rotate_err
966
+ else:
967
+ print(f"[CLOUDFLARE] Tunnel connected and ready: {self.url}")
968
+
969
+ logger.info(f"Using managed tunnel: {self.url}")
970
+ elif mode == "quick":
971
+ # Quick tunnel mode: create tunnel with DNS verification and retry
972
+ # Cloudflare quick tunnels can be flaky - retry with fresh tunnels if needed
973
+ ensure_cloudflared_installed()
974
+
975
+ api_key = self.api_key or self._get_api_key()
976
+ max_tunnel_attempts = int(os.getenv("SYNTH_TUNNEL_MAX_ATTEMPTS", "3"))
977
+
978
+ for tunnel_attempt in range(max_tunnel_attempts):
979
+ if tunnel_attempt > 0:
980
+ logger.warning(
981
+ f"Tunnel attempt {tunnel_attempt + 1}/{max_tunnel_attempts} - "
982
+ "requesting fresh tunnel..."
983
+ )
984
+ # Kill the previous tunnel process if it exists
985
+ if self._tunnel_proc:
986
+ try:
987
+ self._tunnel_proc.terminate()
988
+ await asyncio.sleep(1)
989
+ except Exception:
990
+ pass
991
+
992
+ logger.info("Opening Cloudflare quick tunnel...")
993
+ try:
994
+ self.url, self._tunnel_proc = await open_quick_tunnel_with_dns_verification(
995
+ self.port, api_key=api_key
996
+ )
997
+ except Exception as e:
998
+ logger.warning(f"Tunnel creation failed: {e}")
999
+ if tunnel_attempt == max_tunnel_attempts - 1:
1000
+ raise
1001
+ continue
1002
+
1003
+ # Apply hostname override if provided
1004
+ if override_host:
1005
+ parsed = urlparse(self.url)
1006
+ self.url = f"{parsed.scheme}://{override_host}"
1007
+ logger.info(f"Overriding hostname: {self.url}")
1008
+
1009
+ logger.info(f"Tunnel opened: {self.url}")
1010
+
1011
+ # Extra guard: wait for tunnel HTTP routing to become ready (not just DNS)
1012
+ ready = await _verify_tunnel_ready(
1013
+ self.url,
1014
+ api_key,
1015
+ verify_tls=_should_verify_tls(),
1016
+ )
1017
+ if ready:
1018
+ logger.info(f"Tunnel verified and ready: {self.url}")
1019
+ break
1020
+ else:
1021
+ logger.warning(
1022
+ f"Tunnel {self.url} not routing traffic after verification. "
1023
+ f"{'Retrying with fresh tunnel...' if tunnel_attempt < max_tunnel_attempts - 1 else 'Giving up.'}"
1024
+ )
1025
+ if tunnel_attempt == max_tunnel_attempts - 1:
1026
+ raise RuntimeError(
1027
+ f"Failed to establish working tunnel after {max_tunnel_attempts} attempts. "
1028
+ f"Last tunnel URL: {self.url}. "
1029
+ "This may indicate Cloudflare rate limiting or network issues. "
1030
+ "Try: SYNTH_TUNNEL_MODE=local if the backend can reach localhost, "
1031
+ "or use a named Cloudflare tunnel instead of quick tunnels."
1032
+ )
1033
+ else:
1034
+ raise ValueError(f"Unknown SYNTH_TUNNEL_MODE: {mode}")
1035
+
1036
+ # Register for signal handling
1037
+ _registered_instances.add(self)
1038
+
1039
+ # Call on_start callback if provided
1040
+ if self.on_start:
1041
+ try:
1042
+ self.on_start(self)
1043
+ except Exception as e:
1044
+ logger.warning(f"on_start callback raised exception: {e}")
1045
+
1046
+ return self
1047
+
1048
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
1049
+ """Stop tunnel and server."""
1050
+ logger.info("Stopping in-process task app...")
1051
+
1052
+ # Unregister from signal handling
1053
+ _registered_instances.discard(self)
1054
+
1055
+ # Call on_stop callback if provided
1056
+ if self.on_stop:
1057
+ try:
1058
+ self.on_stop(self)
1059
+ except Exception as e:
1060
+ logger.warning(f"on_stop callback raised exception: {e}")
1061
+
1062
+ # Stop tunnel
1063
+ if self._tunnel_proc:
1064
+ logger.debug("Stopping Cloudflare tunnel...")
1065
+ stop_tunnel(self._tunnel_proc)
1066
+ self._tunnel_proc = None
1067
+ logger.info("Tunnel stopped")
1068
+
1069
+ # Stop the uvicorn server thread gracefully to avoid killing the host process
1070
+ if self._server_thread and self._server_thread.is_alive():
1071
+ logger.debug("Stopping uvicorn server thread...")
1072
+ if self._uvicorn_server:
1073
+ self._uvicorn_server.should_exit = True
1074
+ self._server_thread.join(timeout=5.0)
1075
+ if self._server_thread.is_alive():
1076
+ if self._uvicorn_server:
1077
+ # Last resort if graceful shutdown hangs
1078
+ self._uvicorn_server.force_exit = True
1079
+ self._server_thread.join(timeout=1.0)
1080
+ if self._server_thread.is_alive():
1081
+ logger.warning(
1082
+ "Uvicorn server thread did not stop cleanly; "
1083
+ "it will exit with the main process"
1084
+ )
1085
+ self._server_thread = None
1086
+ self._uvicorn_server = None
1087
+
1088
+ def _get_api_key(self) -> str:
1089
+ """Get API key from environment or default."""
1090
+ import os
1091
+
1092
+ # Try to load .env file if available
1093
+ try:
1094
+ from dotenv import load_dotenv
1095
+ load_dotenv(override=False)
1096
+ except ImportError:
1097
+ pass
1098
+
1099
+ return os.getenv("ENVIRONMENT_API_KEY", "test")
1100
+
1101
+ async def _fetch_tunnel_config(self, api_key: str) -> dict:
1102
+ """Fetch the customer's tunnel configuration from the backend.
1103
+
1104
+ Uses the existing /api/v1/tunnels/tunnel endpoint to get the customer's
1105
+ active tunnels. Returns the first active tunnel's config.
1106
+
1107
+ Returns a dict with:
1108
+ - hostname: The customer's configured tunnel hostname (e.g., "myapp.usesynth.ai")
1109
+ - tunnel_token: The cloudflared tunnel token for running the tunnel
1110
+ - local_port: The local port the tunnel routes to
1111
+ - local_host: The local host the tunnel routes to
1112
+ """
1113
+ from synth_ai.core.env import get_backend_url
1114
+
1115
+ backend_url = get_backend_url()
1116
+ url = f"{backend_url}/api/v1/tunnels/"
1117
+
1118
+ async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
1119
+ try:
1120
+ resp = await client.get(
1121
+ url,
1122
+ headers={
1123
+ "Authorization": f"Bearer {api_key}",
1124
+ "X-API-Key": api_key,
1125
+ },
1126
+ )
1127
+
1128
+ if resp.status_code == 404:
1129
+ logger.debug("No tunnels found for this API key")
1130
+ return {}
1131
+
1132
+ if resp.status_code == 401:
1133
+ raise RuntimeError(
1134
+ "Invalid API key. Please check your SYNTH_API_KEY."
1135
+ )
1136
+
1137
+ resp.raise_for_status()
1138
+ tunnels = resp.json()
1139
+
1140
+ # Return the first active tunnel
1141
+ if tunnels and len(tunnels) > 0:
1142
+ tunnel = tunnels[0]
1143
+ return {
1144
+ "hostname": tunnel.get("hostname"),
1145
+ "tunnel_token": tunnel.get("tunnel_token"),
1146
+ "local_port": tunnel.get("local_port", 8000),
1147
+ "local_host": tunnel.get("local_host", "127.0.0.1"),
1148
+ }
1149
+
1150
+ return {}
1151
+
1152
+ except httpx.HTTPStatusError as e:
1153
+ logger.warning(f"Failed to fetch tunnel config: {e}")
1154
+ return {}
1155
+ except Exception as e:
1156
+ logger.debug(f"Tunnel config fetch failed: {e}")
1157
+ return {}
1158
+
1159
+
1160
+ def _setup_signal_handlers() -> None:
1161
+ """Set up signal handlers for graceful shutdown."""
1162
+
1163
+ def signal_handler(signum, frame):
1164
+ """Handle SIGINT/SIGTERM by cleaning up all registered instances."""
1165
+ logger.info(f"Received signal {signum}, cleaning up {len(_registered_instances)} instances...")
1166
+ for instance in list(_registered_instances):
1167
+ try:
1168
+ # Trigger cleanup
1169
+ if instance._tunnel_proc:
1170
+ stop_tunnel(instance._tunnel_proc)
1171
+ instance._tunnel_proc = None
1172
+ except Exception as e:
1173
+ logger.error(f"Error cleaning up instance: {e}")
1174
+ _registered_instances.clear()
1175
+
1176
+ # Register handlers (only once)
1177
+ if not hasattr(_setup_signal_handlers, "_registered"):
1178
+ signal.signal(signal.SIGINT, signal_handler) # type: ignore[misc]
1179
+ signal.signal(signal.SIGTERM, signal_handler) # type: ignore[misc]
1180
+ _setup_signal_handlers._registered = True # type: ignore[attr-defined]
1181
+
1182
+
1183
+ def _should_verify_tls() -> bool:
1184
+ """Return True unless explicitly disabled via env."""
1185
+ val = (os.getenv("SYNTH_TUNNEL_VERIFY_TLS") or "true").strip().lower()
1186
+ return val not in ("0", "false", "no", "off")
1187
+
1188
+
1189
+ # Set up signal handlers on module import
1190
+ _setup_signal_handlers()