synth-ai 0.2.16__py3-none-any.whl → 0.2.19__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 (299) hide show
  1. examples/analyze_semantic_words.sh +2 -2
  2. examples/baseline/banking77_baseline.py +204 -0
  3. examples/baseline/crafter_baseline.py +407 -0
  4. examples/baseline/pokemon_red_baseline.py +326 -0
  5. examples/baseline/simple_baseline.py +56 -0
  6. examples/baseline/warming_up_to_rl_baseline.py +239 -0
  7. examples/blog_posts/gepa/README.md +355 -0
  8. examples/blog_posts/gepa/configs/banking77_gepa_local.toml +95 -0
  9. examples/blog_posts/gepa/configs/banking77_gepa_test.toml +82 -0
  10. examples/blog_posts/gepa/configs/banking77_mipro_local.toml +52 -0
  11. examples/blog_posts/gepa/configs/hotpotqa_gepa_local.toml +59 -0
  12. examples/blog_posts/gepa/configs/hotpotqa_gepa_qwen.toml +36 -0
  13. examples/blog_posts/gepa/configs/hotpotqa_mipro_local.toml +53 -0
  14. examples/blog_posts/gepa/configs/hover_gepa_local.toml +59 -0
  15. examples/blog_posts/gepa/configs/hover_gepa_qwen.toml +36 -0
  16. examples/blog_posts/gepa/configs/hover_mipro_local.toml +53 -0
  17. examples/blog_posts/gepa/configs/ifbench_gepa_local.toml +59 -0
  18. examples/blog_posts/gepa/configs/ifbench_gepa_qwen.toml +36 -0
  19. examples/blog_posts/gepa/configs/ifbench_mipro_local.toml +53 -0
  20. examples/blog_posts/gepa/configs/pupa_gepa_local.toml +60 -0
  21. examples/blog_posts/gepa/configs/pupa_mipro_local.toml +54 -0
  22. examples/blog_posts/gepa/deploy_banking77_task_app.sh +41 -0
  23. examples/blog_posts/gepa/gepa_baseline.py +204 -0
  24. examples/blog_posts/gepa/query_prompts_example.py +97 -0
  25. examples/blog_posts/gepa/run_gepa_banking77.sh +87 -0
  26. examples/blog_posts/gepa/task_apps.py +105 -0
  27. examples/blog_posts/gepa/test_gepa_local.sh +67 -0
  28. examples/blog_posts/gepa/verify_banking77_setup.sh +123 -0
  29. examples/blog_posts/pokemon_vl/README.md +98 -0
  30. examples/blog_posts/pokemon_vl/configs/eval_gpt5nano.toml +26 -0
  31. examples/blog_posts/pokemon_vl/configs/eval_qwen3_vl.toml +27 -0
  32. examples/blog_posts/pokemon_vl/configs/eval_rl_final.toml +24 -0
  33. examples/blog_posts/pokemon_vl/configs/filter_high_reward.toml +10 -0
  34. examples/blog_posts/pokemon_vl/configs/train_rl_from_sft.toml +43 -0
  35. examples/blog_posts/pokemon_vl/configs/train_sft_qwen4b_vl.toml +40 -0
  36. examples/blog_posts/pokemon_vl/extract_images.py +239 -0
  37. examples/blog_posts/pokemon_vl/pokemon_vl_baseline.py +326 -0
  38. examples/blog_posts/pokemon_vl/run_eval_extract_images.py +209 -0
  39. examples/blog_posts/pokemon_vl/run_qwen_eval_extract_images.py +212 -0
  40. examples/blog_posts/pokemon_vl/text_box_analysis.md +106 -0
  41. examples/blog_posts/warming_up_to_rl/ARCHITECTURE.md +195 -0
  42. examples/blog_posts/warming_up_to_rl/FINAL_TEST_RESULTS.md +127 -0
  43. examples/blog_posts/warming_up_to_rl/INFERENCE_SUCCESS.md +132 -0
  44. examples/blog_posts/warming_up_to_rl/README.md +158 -0
  45. examples/blog_posts/warming_up_to_rl/SMOKE_TESTING.md +164 -0
  46. examples/blog_posts/warming_up_to_rl/SMOKE_TEST_COMPLETE.md +253 -0
  47. examples/blog_posts/warming_up_to_rl/configs/eval_baseline_qwen32b_10x20.toml +25 -0
  48. examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b.toml +25 -0
  49. examples/blog_posts/warming_up_to_rl/configs/eval_ft_qwen4b_10x20.toml +26 -0
  50. examples/blog_posts/warming_up_to_rl/configs/eval_groq_qwen32b.toml +25 -0
  51. examples/blog_posts/warming_up_to_rl/configs/eval_openai_gpt_oss_120b.toml +29 -0
  52. examples/blog_posts/warming_up_to_rl/configs/filter_high_reward_dataset.toml +10 -0
  53. examples/blog_posts/warming_up_to_rl/configs/smoke_test.toml +75 -0
  54. examples/blog_posts/warming_up_to_rl/configs/train_rl_from_sft.toml +91 -0
  55. examples/blog_posts/warming_up_to_rl/configs/train_sft_qwen4b.toml +40 -0
  56. examples/blog_posts/warming_up_to_rl/warming_up_to_rl_baseline.py +187 -0
  57. examples/dev/qwen3_32b_qlora_4xh100.toml +5 -0
  58. examples/multi_step/configs/VERILOG_REWARDS.md +4 -0
  59. examples/multi_step/configs/VERILOG_RL_CHECKLIST.md +4 -0
  60. examples/multi_step/configs/crafter_rl_outcome.toml +2 -1
  61. examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +65 -107
  62. examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +2 -1
  63. examples/multi_step/configs/crafter_rl_stepwise_simple.toml +2 -1
  64. examples/multi_step/configs/crafter_rl_stepwise_simple_NEW_FORMAT.toml +105 -0
  65. examples/multi_step/configs/verilog_rl_lora.toml +80 -123
  66. examples/qwen_coder/configs/coder_lora_30b.toml +1 -3
  67. examples/qwen_coder/configs/coder_lora_4b.toml +4 -1
  68. examples/qwen_coder/configs/coder_lora_small.toml +1 -3
  69. examples/qwen_vl/README.md +10 -12
  70. examples/qwen_vl/SETUP_COMPLETE.md +7 -8
  71. examples/qwen_vl/VISION_TESTS_COMPLETE.md +2 -3
  72. examples/qwen_vl/collect_data_via_cli.md +76 -84
  73. examples/qwen_vl/collect_vision_traces.py +4 -4
  74. examples/qwen_vl/configs/crafter_rl_vision_qwen3vl4b.toml +40 -57
  75. examples/qwen_vl/configs/crafter_vlm_sft_example.toml +1 -2
  76. examples/qwen_vl/configs/eval_gpt4o_mini_vision.toml +20 -37
  77. examples/qwen_vl/configs/eval_gpt5nano_vision.toml +21 -40
  78. examples/qwen_vl/configs/eval_qwen3vl_vision.toml +26 -0
  79. examples/qwen_vl/configs/{filter_qwen2vl_sft.toml → filter_qwen3vl_sft.toml} +4 -5
  80. examples/qwen_vl/configs/filter_vision_sft.toml +2 -3
  81. examples/qwen_vl/crafter_qwen_vl_agent.py +5 -5
  82. examples/qwen_vl/run_vision_comparison.sh +6 -7
  83. examples/rl/README.md +5 -5
  84. examples/rl/configs/rl_from_base_qwen.toml +26 -1
  85. examples/rl/configs/rl_from_base_qwen17.toml +6 -2
  86. examples/rl/task_app/README.md +1 -2
  87. examples/rl/task_app/math_single_step.py +2 -2
  88. examples/run_crafter_demo.sh +2 -2
  89. examples/sft/README.md +1 -1
  90. examples/sft/configs/crafter_fft_qwen0p6b.toml +4 -1
  91. examples/sft/configs/crafter_lora_qwen0p6b.toml +4 -1
  92. examples/swe/task_app/README.md +32 -2
  93. examples/swe/task_app/grpo_swe_mini.py +4 -0
  94. examples/swe/task_app/hosted/envs/crafter/react_agent.py +1 -1
  95. examples/swe/task_app/hosted/envs/mini_swe/environment.py +37 -10
  96. examples/swe/task_app/hosted/inference/openai_client.py +4 -38
  97. examples/swe/task_app/hosted/policy_routes.py +17 -0
  98. examples/swe/task_app/hosted/rollout.py +4 -2
  99. examples/swe/task_app/morph_backend.py +178 -0
  100. examples/task_apps/banking77/__init__.py +6 -0
  101. examples/task_apps/banking77/banking77_task_app.py +841 -0
  102. examples/task_apps/banking77/deploy_wrapper.py +46 -0
  103. examples/task_apps/crafter/CREATE_SFT_DATASET.md +4 -0
  104. examples/task_apps/crafter/FILTER_COMMAND_STATUS.md +4 -0
  105. examples/task_apps/crafter/FILTER_COMMAND_SUCCESS.md +4 -0
  106. examples/task_apps/crafter/task_app/README.md +1 -1
  107. examples/task_apps/crafter/task_app/grpo_crafter.py +90 -5
  108. examples/task_apps/crafter/task_app/grpo_crafter_task_app.py +1 -1
  109. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +4 -26
  110. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/react_agent.py +1 -2
  111. examples/task_apps/crafter/task_app/synth_envs_hosted/hosted_app.py +49 -0
  112. examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +372 -107
  113. examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +81 -12
  114. examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +82 -11
  115. examples/task_apps/crafter/task_app/synth_envs_hosted/utils.py +194 -1
  116. examples/task_apps/enron/task_app/grpo_enron_task_app.py +1 -1
  117. examples/task_apps/gepa_benchmarks/__init__.py +7 -0
  118. examples/task_apps/gepa_benchmarks/common.py +260 -0
  119. examples/task_apps/gepa_benchmarks/hotpotqa_task_app.py +507 -0
  120. examples/task_apps/gepa_benchmarks/hover_task_app.py +436 -0
  121. examples/task_apps/gepa_benchmarks/ifbench_task_app.py +563 -0
  122. examples/task_apps/gepa_benchmarks/pupa_task_app.py +460 -0
  123. examples/task_apps/math/README.md +1 -2
  124. examples/task_apps/pokemon_red/README.md +3 -4
  125. examples/task_apps/pokemon_red/README_IMAGE_ONLY_EVAL.md +4 -0
  126. examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +6 -5
  127. examples/task_apps/pokemon_red/eval_pokemon_red_policy.py +1 -2
  128. examples/task_apps/pokemon_red/task_app.py +288 -39
  129. examples/task_apps/sokoban/README.md +2 -3
  130. examples/task_apps/verilog/eval_groq_qwen32b.toml +12 -14
  131. examples/task_apps/verilog/task_app/grpo_verilog_task_app.py +1 -1
  132. examples/vlm/configs/crafter_vlm_gpt4o.toml +4 -1
  133. examples/warming_up_to_rl/configs/crafter_fft.toml +4 -1
  134. examples/warming_up_to_rl/configs/crafter_fft_4b.toml +0 -2
  135. examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +3 -2
  136. examples/warming_up_to_rl/run_local_rollout_traced.py +1 -1
  137. examples/warming_up_to_rl/task_app/README.md +1 -1
  138. examples/warming_up_to_rl/task_app/grpo_crafter.py +185 -5
  139. examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +1 -1
  140. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +3 -27
  141. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +1 -1
  142. examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +49 -0
  143. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +156 -45
  144. examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +37 -4
  145. examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +33 -3
  146. examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +67 -0
  147. examples/workflows/math_rl/configs/rl_from_base_qwen.toml +27 -0
  148. examples/workflows/math_rl/configs/rl_from_base_qwen17.toml +6 -0
  149. synth_ai/api/train/builders.py +99 -4
  150. synth_ai/api/train/cli.py +516 -26
  151. synth_ai/api/train/config_finder.py +13 -2
  152. synth_ai/api/train/configs/__init__.py +23 -2
  153. synth_ai/api/train/configs/prompt_learning.py +442 -0
  154. synth_ai/api/train/configs/rl.py +61 -7
  155. synth_ai/api/train/configs/sft.py +6 -2
  156. synth_ai/api/train/configs/shared.py +59 -2
  157. synth_ai/api/train/task_app.py +1 -1
  158. synth_ai/api/train/validators.py +277 -0
  159. synth_ai/auth/credentials.py +119 -0
  160. synth_ai/baseline/__init__.py +25 -0
  161. synth_ai/baseline/config.py +209 -0
  162. synth_ai/baseline/discovery.py +214 -0
  163. synth_ai/baseline/execution.py +146 -0
  164. synth_ai/cli/__init__.py +94 -18
  165. synth_ai/cli/__main__.py +0 -0
  166. synth_ai/cli/claude.py +70 -0
  167. synth_ai/cli/codex.py +84 -0
  168. synth_ai/cli/commands/__init__.py +18 -0
  169. synth_ai/cli/commands/baseline/__init__.py +12 -0
  170. synth_ai/cli/commands/baseline/core.py +637 -0
  171. synth_ai/cli/commands/baseline/list.py +93 -0
  172. synth_ai/cli/commands/demo/__init__.py +6 -0
  173. synth_ai/cli/commands/demo/core.py +163 -0
  174. synth_ai/cli/commands/eval/__init__.py +19 -0
  175. synth_ai/cli/commands/eval/core.py +1112 -0
  176. synth_ai/cli/commands/eval/errors.py +81 -0
  177. synth_ai/cli/commands/eval/validation.py +133 -0
  178. synth_ai/cli/commands/filter/__init__.py +12 -0
  179. synth_ai/cli/commands/filter/core.py +424 -0
  180. synth_ai/cli/commands/filter/errors.py +55 -0
  181. synth_ai/cli/commands/filter/validation.py +77 -0
  182. synth_ai/cli/commands/help/__init__.py +177 -0
  183. synth_ai/cli/commands/help/core.py +72 -0
  184. synth_ai/cli/commands/smoke/__init__.py +7 -0
  185. synth_ai/cli/commands/smoke/core.py +1436 -0
  186. synth_ai/cli/commands/status/__init__.py +64 -0
  187. synth_ai/cli/commands/status/client.py +192 -0
  188. synth_ai/cli/commands/status/config.py +92 -0
  189. synth_ai/cli/commands/status/errors.py +20 -0
  190. synth_ai/cli/commands/status/formatters.py +164 -0
  191. synth_ai/cli/commands/status/subcommands/__init__.py +9 -0
  192. synth_ai/cli/commands/status/subcommands/files.py +79 -0
  193. synth_ai/cli/commands/status/subcommands/jobs.py +334 -0
  194. synth_ai/cli/commands/status/subcommands/models.py +79 -0
  195. synth_ai/cli/commands/status/subcommands/pricing.py +22 -0
  196. synth_ai/cli/commands/status/subcommands/runs.py +81 -0
  197. synth_ai/cli/commands/status/subcommands/summary.py +47 -0
  198. synth_ai/cli/commands/status/subcommands/usage.py +203 -0
  199. synth_ai/cli/commands/status/utils.py +114 -0
  200. synth_ai/cli/commands/train/__init__.py +53 -0
  201. synth_ai/cli/commands/train/core.py +21 -0
  202. synth_ai/cli/commands/train/errors.py +117 -0
  203. synth_ai/cli/commands/train/judge_schemas.py +200 -0
  204. synth_ai/cli/commands/train/judge_validation.py +305 -0
  205. synth_ai/cli/commands/train/validation.py +386 -0
  206. synth_ai/cli/demo.py +30 -158
  207. synth_ai/cli/deploy/__init__.py +43 -0
  208. synth_ai/cli/deploy.py +162 -0
  209. synth_ai/cli/eval/__init__.py +36 -0
  210. synth_ai/cli/eval/core.py +5 -0
  211. synth_ai/cli/eval/errors.py +31 -0
  212. synth_ai/cli/eval/validation.py +5 -0
  213. synth_ai/cli/filter/__init__.py +28 -0
  214. synth_ai/cli/filter/core.py +5 -0
  215. synth_ai/cli/filter/errors.py +23 -0
  216. synth_ai/cli/filter/validation.py +5 -0
  217. synth_ai/cli/legacy_root_backup.py +14 -8
  218. synth_ai/cli/modal_serve/__init__.py +12 -0
  219. synth_ai/cli/modal_serve/core.py +14 -0
  220. synth_ai/cli/modal_serve/errors.py +8 -0
  221. synth_ai/cli/modal_serve/validation.py +11 -0
  222. synth_ai/cli/opencode.py +107 -0
  223. synth_ai/cli/root.py +9 -5
  224. synth_ai/cli/serve/__init__.py +12 -0
  225. synth_ai/cli/serve/core.py +14 -0
  226. synth_ai/cli/serve/errors.py +8 -0
  227. synth_ai/cli/serve/validation.py +11 -0
  228. synth_ai/cli/setup.py +20 -265
  229. synth_ai/cli/status.py +7 -126
  230. synth_ai/cli/task_app_deploy.py +1 -10
  231. synth_ai/cli/task_app_modal_serve.py +4 -9
  232. synth_ai/cli/task_app_serve.py +4 -11
  233. synth_ai/cli/task_apps.py +51 -1480
  234. synth_ai/cli/train/__init__.py +12 -0
  235. synth_ai/cli/train/core.py +21 -0
  236. synth_ai/cli/train/errors.py +8 -0
  237. synth_ai/cli/train/validation.py +24 -0
  238. synth_ai/cli/train.py +1 -14
  239. synth_ai/demos/crafter/grpo_crafter_task_app.py +1 -1
  240. synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +1 -1
  241. synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +7 -4
  242. synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +9 -5
  243. synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +4 -3
  244. synth_ai/environments/examples/red/engine.py +33 -12
  245. synth_ai/environments/examples/red/engine_helpers/reward_components.py +151 -179
  246. synth_ai/environments/examples/red/environment.py +26 -0
  247. synth_ai/environments/examples/red/trace_hooks_v3.py +168 -0
  248. synth_ai/http.py +12 -0
  249. synth_ai/judge_schemas.py +10 -10
  250. synth_ai/learning/__init__.py +10 -0
  251. synth_ai/learning/prompt_learning_client.py +276 -0
  252. synth_ai/learning/prompt_learning_types.py +184 -0
  253. synth_ai/learning/rl/client.py +3 -1
  254. synth_ai/pricing/__init__.py +2 -0
  255. synth_ai/pricing/model_pricing.py +57 -0
  256. synth_ai/streaming/__init__.py +29 -0
  257. synth_ai/streaming/config.py +94 -0
  258. synth_ai/streaming/handlers.py +518 -0
  259. synth_ai/streaming/streamer.py +320 -0
  260. synth_ai/streaming/types.py +95 -0
  261. synth_ai/task/apps/__init__.py +1 -0
  262. synth_ai/task/config.py +2 -0
  263. synth_ai/task/tracing_utils.py +25 -25
  264. synth_ai/task/validators.py +45 -9
  265. synth_ai/task_app_cfgs.py +21 -0
  266. synth_ai/tracing_v3/config.py +162 -19
  267. synth_ai/tracing_v3/constants.py +1 -1
  268. synth_ai/tracing_v3/db_config.py +24 -38
  269. synth_ai/tracing_v3/migration_helper.py +1 -2
  270. synth_ai/tracing_v3/storage/config.py +47 -13
  271. synth_ai/tracing_v3/storage/factory.py +3 -3
  272. synth_ai/tracing_v3/turso/daemon.py +113 -11
  273. synth_ai/tracing_v3/turso/native_manager.py +92 -16
  274. synth_ai/types.py +8 -0
  275. synth_ai/urls.py +11 -0
  276. synth_ai/utils/__init__.py +30 -1
  277. synth_ai/utils/agents.py +74 -0
  278. synth_ai/utils/bin.py +39 -0
  279. synth_ai/utils/cli.py +149 -5
  280. synth_ai/utils/env.py +40 -33
  281. synth_ai/utils/http.py +4 -1
  282. synth_ai/utils/json.py +72 -0
  283. synth_ai/utils/modal.py +285 -3
  284. synth_ai/utils/paths.py +48 -0
  285. synth_ai/utils/uvicorn.py +113 -0
  286. {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/METADATA +109 -6
  287. {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/RECORD +291 -142
  288. examples/qwen_vl/configs/eval_qwen2vl_vision.toml +0 -44
  289. synth_ai/cli/tui.py +0 -62
  290. synth_ai/tui/__init__.py +0 -5
  291. synth_ai/tui/__main__.py +0 -13
  292. synth_ai/tui/cli/__init__.py +0 -1
  293. synth_ai/tui/cli/query_experiments.py +0 -164
  294. synth_ai/tui/cli/query_experiments_v3.py +0 -164
  295. synth_ai/tui/dashboard.py +0 -911
  296. {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/WHEEL +0 -0
  297. {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/entry_points.txt +0 -0
  298. {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/licenses/LICENSE +0 -0
  299. {synth_ai-0.2.16.dist-info → synth_ai-0.2.19.dist-info}/top_level.txt +0 -0
@@ -1,19 +1,162 @@
1
- """Configuration for tracing v3 with Turso/sqld."""
1
+ """Configuration helpers for tracing v3.
2
2
 
3
- import os
4
- from dataclasses import dataclass
3
+ This module centralises the logic for discovering which datastore the tracer
4
+ should use. Historically the project defaulted to a local SQLite file which
5
+ breaks under parallel load. The new resolver inspects environment variables
6
+ and defaults to Turso/libSQL whenever credentials are supplied, while keeping a
7
+ SQLite fallback for contributors without remote access.
8
+ """
5
9
 
6
- from synth_ai.tracing_v3.constants import canonical_trace_db_path
10
+ from __future__ import annotations
7
11
 
8
- DEFAULT_DB_FILE = str(canonical_trace_db_path())
12
+ import os
13
+ from dataclasses import dataclass, field
14
+ from pathlib import Path
15
+ from typing import Any
16
+ from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
9
17
 
18
+ from synth_ai.tracing_v3.constants import canonical_trace_db_path
10
19
 
11
- def _default_sqlite_url() -> str:
12
- base_path = os.path.abspath(os.getenv("SQLD_DB_PATH", DEFAULT_DB_FILE))
13
- candidate = os.path.join(base_path, "dbs", "default", "data")
14
- if os.path.isdir(base_path) and os.path.exists(candidate):
15
- return f"sqlite+aiosqlite:///{candidate}"
16
- return f"sqlite+aiosqlite:///{base_path}"
20
+ # STARTUP DIAGNOSTIC - Commented out to reduce noise
21
+ # print(f"[TRACING_V3_CONFIG_LOADED] Python={sys.version_info.major}.{sys.version_info.minor} MODAL_IS_REMOTE={os.getenv('MODAL_IS_REMOTE')}", flush=True)
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # DSN resolution helpers
25
+ # ---------------------------------------------------------------------------
26
+
27
+ _CANONICAL_DB_PATH = canonical_trace_db_path()
28
+ _DEFAULT_TRACE_DIR = Path(os.getenv("SYNTH_TRACES_DIR", _CANONICAL_DB_PATH.parent))
29
+
30
+
31
+ def _normalise_path(path: Path) -> Path:
32
+ """Resolve relative paths and expand user/home markers."""
33
+ path = path.expanduser()
34
+ if not path.is_absolute():
35
+ path = (Path.cwd() / path).resolve()
36
+ return path
37
+
38
+
39
+ def _is_modal_environment() -> bool:
40
+ """Detect if running in Modal container.
41
+
42
+ Modal automatically sets MODAL_IS_REMOTE=1 in all deployed containers.
43
+ We check this first, then fall back to other Modal env vars.
44
+ """
45
+ # Modal sets this in all deployed containers
46
+ if os.getenv("MODAL_IS_REMOTE") == "1":
47
+ return True
48
+
49
+ # Additional Modal env vars as fallback
50
+ return bool(
51
+ os.getenv("MODAL_TASK_ID")
52
+ or os.getenv("MODAL_ENVIRONMENT")
53
+ or os.getenv("SERVICE", "").upper() == "MODAL"
54
+ )
55
+
56
+
57
+ def _split_auth_from_url(url: str) -> tuple[str, str | None]:
58
+ """Strip any auth_token query parameter from a DSN."""
59
+ parsed = urlparse(url)
60
+ if not parsed.query:
61
+ return url, None
62
+
63
+ params = dict(parse_qsl(parsed.query, keep_blank_values=True))
64
+ token = params.pop("auth_token", None)
65
+ query = urlencode(params, doseq=True)
66
+ # urlunparse will omit the '?' automatically when query is empty
67
+ sanitised = urlunparse(parsed._replace(query=query))
68
+ return sanitised, token
69
+
70
+
71
+ def _default_sqlite_url(*, ensure_dir: bool = True) -> tuple[str, str | None]:
72
+ """Generate a SQLite URL from SYNTH_TRACES_DIR if set, otherwise raise."""
73
+ traces_dir = os.getenv("SYNTH_TRACES_DIR")
74
+ if traces_dir:
75
+ dir_path = _normalise_path(Path(traces_dir))
76
+ if ensure_dir:
77
+ dir_path.mkdir(parents=True, exist_ok=True)
78
+ db_path = dir_path / "synth_traces.db"
79
+ sqlite_url = f"sqlite+aiosqlite:///{db_path}"
80
+ return sqlite_url, None
81
+ raise RuntimeError("SQLite fallback is disabled; configure LIBSQL_URL or run sqld locally.")
82
+
83
+
84
+ def resolve_trace_db_settings(*, ensure_dir: bool = True) -> tuple[str, str | None]:
85
+ """Resolve the tracing database URL and optional auth token.
86
+
87
+ Resolution order:
88
+ 1. `SYNTH_TRACES_DB` (explicit DSN override)
89
+ 2. `LIBSQL_URL` / `TURSO_DATABASE_URL` (remote libSQL endpoints)
90
+ 3. `TURSO_LOCAL_DB_URL` (legacy env for local sqld)
91
+ 4. Modal environment: plain SQLite file (no sqld, no auth)
92
+ 5. Local dev: sqld default
93
+ """
94
+ import logging
95
+ logger = logging.getLogger(__name__)
96
+
97
+ explicit = os.getenv("SYNTH_TRACES_DB")
98
+ if explicit:
99
+ logger.info(f"[TRACE_CONFIG] Using explicit SYNTH_TRACES_DB: {explicit}")
100
+ return _split_auth_from_url(explicit)
101
+
102
+ remote = os.getenv("LIBSQL_URL") or os.getenv("TURSO_DATABASE_URL")
103
+ if remote:
104
+ logger.info(f"[TRACE_CONFIG] Using remote Turso: {remote}")
105
+ url, token = _split_auth_from_url(remote)
106
+ if token:
107
+ return url, token
108
+ env_token = os.getenv("LIBSQL_AUTH_TOKEN") or os.getenv("TURSO_AUTH_TOKEN")
109
+ return url, env_token
110
+
111
+ local_override = os.getenv("TURSO_LOCAL_DB_URL")
112
+ if local_override:
113
+ logger.info(f"[TRACE_CONFIG] Using TURSO_LOCAL_DB_URL: {local_override}")
114
+ url, token = _split_auth_from_url(local_override)
115
+ if token:
116
+ return url, token
117
+ env_token = os.getenv("LIBSQL_AUTH_TOKEN") or os.getenv("TURSO_AUTH_TOKEN")
118
+ return url, env_token
119
+
120
+ # Check for SYNTH_TRACES_DIR to generate SQLite URL
121
+ traces_dir = os.getenv("SYNTH_TRACES_DIR")
122
+ if traces_dir:
123
+ try:
124
+ sqlite_url, _ = _default_sqlite_url(ensure_dir=ensure_dir)
125
+ logger.info(f"[TRACE_CONFIG] Using SQLite from SYNTH_TRACES_DIR: {sqlite_url}")
126
+ return sqlite_url, None
127
+ except RuntimeError:
128
+ pass # Fall through to other options
129
+
130
+ # Modal environment: use plain SQLite file (no sqld daemon, no auth required)
131
+ is_modal = _is_modal_environment()
132
+ logger.info(f"[TRACE_CONFIG] Modal detection: {is_modal} (MODAL_IS_REMOTE={os.getenv('MODAL_IS_REMOTE')})")
133
+ if is_modal:
134
+ logger.info("[TRACE_CONFIG] Using Modal SQLite: file:/tmp/synth_traces.db")
135
+ return "file:/tmp/synth_traces.db", None
136
+
137
+ # Local dev: default to sqld HTTP API
138
+ default_url = os.getenv("LIBSQL_DEFAULT_URL", "http://127.0.0.1:8081")
139
+ logger.info(f"[TRACE_CONFIG] Using local sqld: {default_url}")
140
+ return default_url, None
141
+
142
+
143
+ def resolve_trace_db_url(*, ensure_dir: bool = True) -> str:
144
+ """Return just the DSN, discarding any auth token."""
145
+ url, _ = resolve_trace_db_settings(ensure_dir=ensure_dir)
146
+ return url
147
+
148
+
149
+ def resolve_trace_db_auth_token() -> str | None:
150
+ """Return the resolved auth token for the tracing datastore."""
151
+ _, token = resolve_trace_db_settings()
152
+ return token
153
+
154
+
155
+ # ---------------------------------------------------------------------------
156
+ # Config dataclasses
157
+ # ---------------------------------------------------------------------------
158
+
159
+ DEFAULT_DB_FILE = str(_normalise_path(_DEFAULT_TRACE_DIR) / _CANONICAL_DB_PATH.name)
17
160
 
18
161
 
19
162
  @dataclass
@@ -24,12 +167,12 @@ class TursoConfig:
24
167
  DEFAULT_DB_FILE = DEFAULT_DB_FILE
25
168
  DEFAULT_HTTP_PORT = 8080
26
169
 
27
- # Use env override if provided; otherwise resolve based on SQLD layout
28
- db_url: str = os.getenv("TURSO_LOCAL_DB_URL", _default_sqlite_url())
170
+ # Resolve DB URL and auth token from environment (libSQL preferred)
171
+ db_url: str = field(default_factory=resolve_trace_db_url)
29
172
 
30
173
  # Remote database sync configuration
31
- sync_url: str = os.getenv("TURSO_DATABASE_URL", "")
32
- auth_token: str = os.getenv("TURSO_AUTH_TOKEN", "")
174
+ sync_url: str = os.getenv("LIBSQL_SYNC_URL") or os.getenv("TURSO_SYNC_URL", "")
175
+ auth_token: str = resolve_trace_db_auth_token() or ""
33
176
  sync_interval: int = int(
34
177
  os.getenv("TURSO_SYNC_SECONDS", "2")
35
178
  ) # 2 seconds for responsive local development
@@ -54,16 +197,16 @@ class TursoConfig:
54
197
  sqld_http_port: int = int(os.getenv("SQLD_HTTP_PORT", "8080"))
55
198
  sqld_idle_shutdown: int = int(os.getenv("SQLD_IDLE_SHUTDOWN", "0")) # 0 = no idle shutdown
56
199
 
57
- def get_connect_args(self) -> dict:
200
+ def get_connect_args(self) -> dict[str, str]:
58
201
  """Get SQLAlchemy connection arguments."""
59
- args = {}
202
+ args: dict[str, str] = {}
60
203
  if self.auth_token:
61
204
  args["auth_token"] = self.auth_token
62
205
  return args
63
206
 
64
- def get_engine_kwargs(self) -> dict:
207
+ def get_engine_kwargs(self) -> dict[str, Any]:
65
208
  """Get SQLAlchemy engine creation kwargs."""
66
- kwargs = {
209
+ kwargs: dict[str, Any] = {
67
210
  "echo": self.echo_sql,
68
211
  "future": True,
69
212
  }
@@ -4,7 +4,7 @@ from datetime import datetime
4
4
  from pathlib import Path
5
5
 
6
6
  TRACE_DB_DIR = Path("traces")
7
- TRACE_DB_BASENAME = "task_app_traces"
7
+ TRACE_DB_BASENAME = "turso_task_app_traces"
8
8
 
9
9
 
10
10
  def canonical_trace_db_name(*, timestamp: datetime | None = None) -> str:
@@ -30,11 +30,12 @@ class DatabaseConfig:
30
30
 
31
31
  Args:
32
32
  db_path: Path to database file. If None, uses DEFAULT_DB_FILE from serve.sh.
33
- http_port: HTTP port for sqld daemon. If None, uses DEFAULT_HTTP_PORT from serve.sh.
33
+ http_port: Hrana WebSocket port for sqld daemon (env: SQLD_HTTP_PORT). If None, uses DEFAULT_HTTP_PORT.
34
34
  use_sqld: Whether to use sqld daemon or direct SQLite.
35
35
  """
36
36
  self.use_sqld = use_sqld and self._sqld_binary_available()
37
- self.http_port = http_port or int(os.getenv("SQLD_HTTP_PORT", self.DEFAULT_HTTP_PORT))
37
+ # Note: SQLD_HTTP_PORT is actually the hrana port (8080), not the HTTP API port
38
+ self.hrana_port = http_port or int(os.getenv("SQLD_HTTP_PORT", self.DEFAULT_HTTP_PORT))
38
39
  self._daemon: SqldDaemon | None = None
39
40
 
40
41
  # Set up database path to match serve.sh configuration
@@ -57,21 +58,16 @@ class DatabaseConfig:
57
58
  abs_path = os.path.abspath(self.db_file)
58
59
  sqld_data_path = os.path.join(abs_path, "dbs", "default", "data")
59
60
 
60
- if os.path.exists(sqld_data_path):
61
- # sqld is managing the database
62
- logger.debug(f" Using sqld-managed database at: {sqld_data_path}")
63
- actual_db_path = sqld_data_path
64
- else:
65
- # Direct SQLite file
66
- if not os.path.exists(abs_path):
67
- logger.debug(f"⚠️ Database file not found at: {abs_path}")
68
- logger.debug("🔧 Make sure to run './serve.sh' to start the turso/sqld service")
69
- else:
70
- logger.debug(f"📁 Using direct SQLite file at: {abs_path}")
71
- actual_db_path = abs_path
61
+ if not os.path.exists(sqld_data_path) and not os.path.exists(abs_path):
62
+ raise RuntimeError(
63
+ "sqld data directory not found. Run `sqld --db-path <path>` before using the tracing database."
64
+ )
72
65
 
73
- # SQLite URLs need 3 slashes for absolute paths
74
- return f"sqlite+aiosqlite:///{actual_db_path}"
66
+ # Use http:// for local sqld HTTP API port
67
+ # sqld has two ports: hrana_port (Hrana WebSocket) and hrana_port+1 (HTTP API)
68
+ # Python libsql client uses HTTP API with http:// URLs
69
+ http_api_port = self.hrana_port + 1
70
+ return f"http://127.0.0.1:{http_api_port}"
75
71
 
76
72
  def _sqld_binary_available(self) -> bool:
77
73
  """Check if the sqld (Turso) binary is available on PATH."""
@@ -84,18 +80,12 @@ class DatabaseConfig:
84
80
  return True
85
81
 
86
82
  if binary_override:
87
- logger.warning(
88
- "Configured SQLD_BINARY='%s' but the executable was not found on PATH. "
89
- "Falling back to direct SQLite.",
90
- binary_override,
83
+ raise RuntimeError(
84
+ f"Configured SQLD_BINARY='{binary_override}' but the executable was not found on PATH."
91
85
  )
92
- else:
93
- logger.warning(
94
- "sqld binary not detected; falling back to SQLite-only mode. "
95
- "Install Turso's sqld or set SQLD_BINARY to enable the Turso daemon."
96
- )
97
-
98
- return False
86
+ raise RuntimeError(
87
+ "sqld binary not detected; install Turso's sqld or set SQLD_BINARY so that libSQL can be used."
88
+ )
99
89
 
100
90
  def start_daemon(self, wait_time: float = 2.0):
101
91
  """
@@ -114,7 +104,7 @@ class DatabaseConfig:
114
104
  # Import here to avoid circular dependency
115
105
  from .turso.daemon import SqldDaemon
116
106
 
117
- self._daemon = SqldDaemon(db_path=self.db_base_path, http_port=self.http_port)
107
+ self._daemon = SqldDaemon(db_path=self.db_base_path, hrana_port=self.hrana_port)
118
108
 
119
109
  self._daemon.start()
120
110
 
@@ -160,11 +150,13 @@ def get_default_db_config() -> DatabaseConfig:
160
150
  # Check if sqld is already running (started by serve.sh)
161
151
  import subprocess
162
152
 
163
- sqld_port = int(os.getenv("SQLD_HTTP_PORT", DatabaseConfig.DEFAULT_HTTP_PORT))
153
+ sqld_hrana_port = int(os.getenv("SQLD_HTTP_PORT", DatabaseConfig.DEFAULT_HTTP_PORT))
154
+ sqld_http_port = sqld_hrana_port + 1
164
155
  sqld_running = False
165
156
  try:
157
+ # Check for either hrana or http port in the process command line
166
158
  result = subprocess.run(
167
- ["pgrep", "-f", f"sqld.*--http-listen-addr.*:{sqld_port}"],
159
+ ["pgrep", "-f", f"sqld.*(--hrana-listen-addr.*:{sqld_hrana_port}|--http-listen-addr.*:{sqld_http_port})"],
168
160
  capture_output=True,
169
161
  text=True,
170
162
  )
@@ -172,18 +164,12 @@ def get_default_db_config() -> DatabaseConfig:
172
164
  # sqld is already running, don't start a new one
173
165
  sqld_running = True
174
166
  use_sqld = False
175
- logger.debug(f"✅ Detected sqld already running on port {sqld_port}")
167
+ logger.debug(f"✅ Detected sqld already running on ports {sqld_hrana_port} (hrana) and {sqld_http_port} (http)")
176
168
  except Exception as e:
177
169
  logger.debug(f"Could not check for sqld process: {e}")
178
170
 
179
171
  if not sqld_running and use_sqld:
180
- logger.warning("⚠️ sqld service not detected!")
181
- logger.warning("🔧 Please start the turso/sqld service by running:")
182
- logger.warning(" ./serve.sh")
183
- logger.warning("")
184
- logger.warning("This will start:")
185
- logger.warning(" - sqld daemon (SQLite server) on port 8080")
186
- logger.warning(" - Environment service on port 8901")
172
+ logger.warning("sqld service not detected. Start the Turso daemon (./serve.sh) before running tracing workloads.")
187
173
 
188
174
  _default_config = DatabaseConfig(db_path=db_path, use_sqld=use_sqld)
189
175
 
@@ -68,7 +68,7 @@ def categorize_files(v2_files: list[tuple[str, list[str]]]) -> dict:
68
68
  categories["examples"].append((file_path, imports))
69
69
  elif any(
70
70
  core in file_path
71
- for core in ["synth_ai/lm/", "synth_ai/tui/", "synth_ai/environments/"]
71
+ for core in ["synth_ai/lm/", "synth_ai/environments/"]
72
72
  ):
73
73
  categories["core_library"].append((file_path, imports))
74
74
  else:
@@ -104,7 +104,6 @@ def print_migration_report():
104
104
  print("2. Debug scripts: Can be deleted or archived")
105
105
  print("3. Core library files: Need careful migration to v3")
106
106
  print(" - synth_ai/lm/core/main_v2.py")
107
- print(" - synth_ai/tui/cli/query_experiments.py")
108
107
  print(" - synth_ai/environments/service/core_routes.py")
109
108
  print("4. Examples: Should be updated to demonstrate v3 usage")
110
109
 
@@ -1,10 +1,12 @@
1
1
  """Storage configuration for tracing v3."""
2
2
 
3
3
  import os
4
- from dataclasses import dataclass
4
+ from dataclasses import dataclass, field
5
5
  from enum import Enum
6
6
  from typing import Any
7
7
 
8
+ from ..config import resolve_trace_db_auth_token, resolve_trace_db_settings
9
+
8
10
 
9
11
  class StorageBackend(str, Enum):
10
12
  """Supported storage backends."""
@@ -24,12 +26,9 @@ def _is_enabled(value: str | None) -> bool:
24
26
  class StorageConfig:
25
27
  """Configuration for storage backend."""
26
28
 
27
- backend: StorageBackend = StorageBackend.TURSO_NATIVE
28
29
  connection_string: str | None = None
29
-
30
- # Turso-specific settings
31
- turso_url: str = os.getenv("TURSO_DATABASE_URL", "sqlite+libsql://http://127.0.0.1:8080")
32
- turso_auth_token: str = os.getenv("TURSO_AUTH_TOKEN", "")
30
+ backend: StorageBackend | None = None
31
+ turso_auth_token: str | None = field(default=None)
33
32
 
34
33
  # Common settings
35
34
  pool_size: int = int(os.getenv("STORAGE_POOL_SIZE", "8"))
@@ -44,9 +43,48 @@ class StorageConfig:
44
43
  # Allow legacy override while keeping compatibility with existing TURSO_NATIVE env flag
45
44
  native_env = os.getenv("TURSO_NATIVE")
46
45
  native_flag = _is_enabled(native_env) if native_env is not None else None
46
+ resolved_url: str | None = self.connection_string
47
+ resolved_token: str | None = self.turso_auth_token
48
+
49
+ if resolved_url is None:
50
+ resolved_url, inferred_token = resolve_trace_db_settings()
51
+ self.connection_string = resolved_url
52
+ resolved_token = inferred_token
53
+
54
+ if resolved_token is None:
55
+ resolved_token = resolve_trace_db_auth_token()
56
+
57
+ self.turso_auth_token = resolved_token or ""
58
+
59
+ if self.backend is None:
60
+ self.backend = self._infer_backend(self.connection_string or "")
47
61
 
48
62
  if native_flag is False:
49
- self.backend = StorageBackend.SQLITE
63
+ raise RuntimeError("TURSO_NATIVE=false is no longer supported; only Turso/libSQL backend is available.")
64
+
65
+ # Allow both TURSO_NATIVE and SQLITE backends (both use libsql.connect)
66
+ if self.backend not in (StorageBackend.TURSO_NATIVE, StorageBackend.SQLITE):
67
+ raise RuntimeError(f"Unsupported backend: {self.backend}. Only Turso/libSQL and SQLite are supported.")
68
+
69
+ @staticmethod
70
+ def _infer_backend(connection_string: str) -> StorageBackend:
71
+ """Infer backend type from the connection string."""
72
+ scheme = connection_string.split(":", 1)[0].lower()
73
+
74
+ # Plain SQLite files: file://, /absolute/path, or no scheme
75
+ if (
76
+ scheme == "file"
77
+ or scheme.startswith("sqlite")
78
+ or connection_string.startswith("/")
79
+ or "://" not in connection_string
80
+ ):
81
+ return StorageBackend.SQLITE
82
+
83
+ # Turso/sqld: libsql://, http://, https://
84
+ if scheme.startswith("libsql") or "libsql" in scheme or scheme in ("http", "https"):
85
+ return StorageBackend.TURSO_NATIVE
86
+
87
+ raise RuntimeError(f"Unsupported tracing backend scheme: {scheme}")
50
88
 
51
89
  def get_connection_string(self) -> str:
52
90
  """Get the appropriate connection string for the backend."""
@@ -54,12 +92,8 @@ class StorageConfig:
54
92
  return self.connection_string
55
93
 
56
94
  if self.backend == StorageBackend.TURSO_NATIVE:
57
- return self.turso_url
58
- if self.backend == StorageBackend.SQLITE:
59
- return "sqlite+aiosqlite:///traces.db"
60
- if self.backend == StorageBackend.POSTGRES:
61
- return os.getenv("POSTGRES_URL", "postgresql+asyncpg://localhost/traces")
62
- raise ValueError(f"Unknown backend: {self.backend}")
95
+ return self.connection_string or ""
96
+ raise ValueError(f"Unsupported backend: {self.backend}")
63
97
 
64
98
  def get_backend_config(self) -> dict[str, Any]:
65
99
  """Get backend-specific configuration."""
@@ -24,14 +24,14 @@ def create_storage(config: StorageConfig | None = None) -> TraceStorage:
24
24
 
25
25
  connection_string = config.get_connection_string()
26
26
 
27
- if config.backend == StorageBackend.TURSO_NATIVE:
27
+ # Both TURSO_NATIVE and SQLITE use NativeLibsqlTraceManager
28
+ # because libsql.connect() handles both remote and local file databases
29
+ if config.backend in (StorageBackend.TURSO_NATIVE, StorageBackend.SQLITE):
28
30
  backend_config = config.get_backend_config()
29
31
  return NativeLibsqlTraceManager(
30
32
  db_url=connection_string,
31
33
  auth_token=backend_config.get("auth_token"),
32
34
  )
33
- elif config.backend == StorageBackend.SQLITE:
34
- return NativeLibsqlTraceManager(db_url=connection_string)
35
35
  elif config.backend == StorageBackend.POSTGRES:
36
36
  # Future: PostgreSQL implementation
37
37
  raise NotImplementedError("PostgreSQL backend not yet implemented")
@@ -1,8 +1,11 @@
1
1
  """sqld daemon management utilities."""
2
2
 
3
+ import logging
4
+ import os
3
5
  import pathlib
4
6
  import shutil
5
7
  import subprocess
8
+ import sys
6
9
  import time
7
10
 
8
11
  import requests
@@ -10,6 +13,8 @@ from requests import RequestException
10
13
 
11
14
  from ..config import CONFIG
12
15
 
16
+ logger = logging.getLogger(__name__)
17
+
13
18
 
14
19
  class SqldDaemon:
15
20
  """Manages local sqld daemon lifecycle."""
@@ -18,28 +23,101 @@ class SqldDaemon:
18
23
  self,
19
24
  db_path: str | None = None,
20
25
  http_port: int | None = None,
26
+ hrana_port: int | None = None,
21
27
  binary_path: str | None = None,
22
28
  ):
23
29
  """Initialize sqld daemon manager.
24
30
 
25
31
  Args:
26
32
  db_path: Path to database file (uses config default if not provided)
27
- http_port: HTTP port for daemon (uses config default if not provided)
33
+ http_port: HTTP port for health/API (uses config default + 1 if not provided)
34
+ hrana_port: Hrana WebSocket port for libsql connections (uses config default if not provided)
28
35
  binary_path: Path to sqld binary (auto-detected if not provided)
29
36
  """
30
37
  self.db_path = db_path or CONFIG.sqld_db_path
31
- self.http_port = http_port or CONFIG.sqld_http_port
38
+ self.hrana_port = hrana_port or CONFIG.sqld_http_port # Main port for libsql://
39
+ self.http_port = http_port or (self.hrana_port + 1) # HTTP API on next port
32
40
  self.binary_path = binary_path or self._find_binary()
33
41
  self.process: subprocess.Popen[str] | None = None
34
42
 
35
43
  def _find_binary(self) -> str:
36
- """Find sqld binary in PATH."""
44
+ """Find sqld binary in PATH, auto-installing if needed.
45
+
46
+ Search order:
47
+ 1. CONFIG.sqld_binary in PATH
48
+ 2. libsql-server in PATH
49
+ 3. Common install locations (~/.turso/bin, /usr/local/bin, etc.)
50
+ 4. Auto-install via synth_ai.utils.sqld (if interactive terminal)
51
+
52
+ Returns:
53
+ Path to sqld binary
54
+
55
+ Raises:
56
+ RuntimeError: If binary not found and auto-install fails/disabled
57
+ """
58
+ # Check PATH first
37
59
  binary = shutil.which(CONFIG.sqld_binary) or shutil.which("libsql-server")
38
- if not binary:
39
- raise RuntimeError(
40
- "sqld binary not found in PATH. Install with: brew install turso-tech/tools/sqld"
41
- )
42
- return binary
60
+ if binary:
61
+ logger.debug(f"Found sqld binary in PATH: {binary}")
62
+ return binary
63
+
64
+ # Check common install locations
65
+ try:
66
+ from synth_ai.utils.sqld import find_sqld_binary
67
+ binary = find_sqld_binary()
68
+ if binary:
69
+ logger.debug(f"Found sqld binary in common location: {binary}")
70
+ return binary
71
+ except ImportError:
72
+ logger.debug("synth_ai.utils.sqld not available, skipping common location check")
73
+
74
+ # Try auto-install if enabled and interactive
75
+ auto_install_enabled = os.getenv("SYNTH_AI_AUTO_INSTALL_SQLD", "true").lower() == "true"
76
+
77
+ if auto_install_enabled and sys.stdin.isatty():
78
+ try:
79
+ from synth_ai.utils.sqld import install_sqld
80
+ logger.info("sqld binary not found. Attempting automatic installation...")
81
+
82
+ # Use click if available for better UX, otherwise proceed automatically
83
+ try:
84
+ import click
85
+ if not click.confirm(
86
+ "sqld not found. Install automatically via Homebrew?",
87
+ default=True
88
+ ):
89
+ raise RuntimeError("User declined automatic installation")
90
+ except ImportError:
91
+ # click not available, auto-install without prompt
92
+ logger.info("Installing sqld automatically (non-interactive mode)")
93
+
94
+ binary = install_sqld()
95
+ logger.info(f"Successfully installed sqld to: {binary}")
96
+ return binary
97
+
98
+ except Exception as exc:
99
+ logger.warning(f"Auto-install failed: {exc}")
100
+ # Fall through to error message below
101
+ elif not auto_install_enabled:
102
+ logger.debug("Auto-install disabled via SYNTH_AI_AUTO_INSTALL_SQLD=false")
103
+ elif not sys.stdin.isatty():
104
+ logger.debug("Non-interactive terminal, skipping auto-install prompt")
105
+
106
+ # If we get here, all methods failed
107
+ raise RuntimeError(
108
+ "sqld binary not found. Install using one of these methods:\n"
109
+ "\n"
110
+ "Quick install (recommended):\n"
111
+ " synth-ai turso\n"
112
+ "\n"
113
+ "Manual install:\n"
114
+ " brew install turso-tech/tools/sqld\n"
115
+ " # or\n"
116
+ " curl -sSfL https://get.tur.so/install.sh | bash && turso dev\n"
117
+ "\n"
118
+ "For CI/CD environments:\n"
119
+ " Set SYNTH_AI_AUTO_INSTALL_SQLD=false and pre-install sqld"
120
+ )
43
121
 
44
122
  def start(self, wait_for_ready: bool = True) -> subprocess.Popen:
45
123
  """Start the sqld daemon."""
@@ -53,6 +131,8 @@ class SqldDaemon:
53
131
  self.binary_path,
54
132
  "--db-path",
55
133
  str(db_file),
134
+ "--hrana-listen-addr",
135
+ f"127.0.0.1:{self.hrana_port}",
56
136
  "--http-listen-addr",
57
137
  f"127.0.0.1:{self.http_port}",
58
138
  ]
@@ -112,6 +192,14 @@ class SqldDaemon:
112
192
  """Check if daemon is running."""
113
193
  return self.process is not None and self.process.poll() is None
114
194
 
195
+ def get_hrana_port(self) -> int:
196
+ """Get the Hrana WebSocket port for libsql:// connections."""
197
+ return self.hrana_port
198
+
199
+ def get_http_port(self) -> int:
200
+ """Get the HTTP API port for health checks."""
201
+ return self.http_port
202
+
115
203
  def __enter__(self):
116
204
  """Context manager entry."""
117
205
  self.start()
@@ -126,13 +214,27 @@ class SqldDaemon:
126
214
  _daemon: SqldDaemon | None = None
127
215
 
128
216
 
129
- def start_sqld(db_path: str | None = None, port: int | None = None) -> SqldDaemon:
130
- """Start a global sqld daemon instance."""
217
+ def start_sqld(
218
+ db_path: str | None = None,
219
+ port: int | None = None,
220
+ hrana_port: int | None = None,
221
+ http_port: int | None = None,
222
+ ) -> SqldDaemon:
223
+ """Start a global sqld daemon instance.
224
+
225
+ Args:
226
+ db_path: Path to database file
227
+ port: Legacy parameter - used as hrana_port if hrana_port not specified
228
+ hrana_port: Hrana WebSocket port for libsql:// connections
229
+ http_port: HTTP API port for health checks
230
+ """
131
231
  global _daemon
132
232
  if _daemon and _daemon.is_running():
133
233
  return _daemon
134
234
 
135
- _daemon = SqldDaemon(db_path=db_path, http_port=port)
235
+ # Support legacy 'port' parameter by using it as hrana_port
236
+ final_hrana_port = hrana_port or port
237
+ _daemon = SqldDaemon(db_path=db_path, hrana_port=final_hrana_port, http_port=http_port)
136
238
  _daemon.start()
137
239
  return _daemon
138
240