synth-ai 0.2.13.dev2__py3-none-any.whl → 0.2.16__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 (293) hide show
  1. examples/README.md +1 -0
  2. examples/multi_step/SFT_README.md +147 -0
  3. examples/multi_step/configs/README_verilog_rl.md +77 -0
  4. examples/multi_step/configs/VERILOG_REWARDS.md +90 -0
  5. examples/multi_step/configs/VERILOG_RL_CHECKLIST.md +183 -0
  6. examples/multi_step/configs/crafter_eval_synth_qwen4b.toml +35 -0
  7. examples/multi_step/configs/crafter_eval_text_only_groq_qwen32b.toml +36 -0
  8. examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +12 -11
  9. examples/multi_step/configs/crafter_sft_qwen30b_lora.toml +62 -0
  10. examples/multi_step/configs/crafter_synth_backend.md +40 -0
  11. examples/multi_step/configs/verilog_eval_groq_qwen32b.toml +31 -0
  12. examples/multi_step/configs/verilog_eval_synth_qwen8b.toml +33 -0
  13. examples/multi_step/configs/verilog_rl_lora.toml +190 -0
  14. examples/multi_step/convert_traces_to_sft.py +84 -0
  15. examples/multi_step/judges/crafter_backend_judge.py +220 -0
  16. examples/multi_step/judges/verilog_backend_judge.py +234 -0
  17. examples/multi_step/readme.md +48 -0
  18. examples/multi_step/run_sft_qwen30b.sh +45 -0
  19. examples/multi_step/verilog_rl_lora.md +218 -0
  20. examples/qwen_coder/configs/coder_lora_30b.toml +3 -2
  21. examples/qwen_coder/configs/coder_lora_4b.toml +2 -1
  22. examples/qwen_coder/configs/coder_lora_small.toml +2 -1
  23. examples/qwen_vl/BUGS_AND_FIXES.md +232 -0
  24. examples/qwen_vl/IMAGE_VALIDATION_COMPLETE.md +271 -0
  25. examples/qwen_vl/IMAGE_VALIDATION_SUMMARY.md +260 -0
  26. examples/qwen_vl/INFERENCE_SFT_TESTS.md +412 -0
  27. examples/qwen_vl/NEXT_STEPS_2B.md +325 -0
  28. examples/qwen_vl/QUICKSTART.md +327 -0
  29. examples/qwen_vl/QUICKSTART_RL_VISION.md +110 -0
  30. examples/qwen_vl/README.md +154 -0
  31. examples/qwen_vl/RL_VISION_COMPLETE.md +475 -0
  32. examples/qwen_vl/RL_VISION_TESTING.md +333 -0
  33. examples/qwen_vl/SDK_VISION_INTEGRATION.md +328 -0
  34. examples/qwen_vl/SETUP_COMPLETE.md +275 -0
  35. examples/qwen_vl/VISION_TESTS_COMPLETE.md +490 -0
  36. examples/qwen_vl/VLM_PIPELINE_COMPLETE.md +242 -0
  37. examples/qwen_vl/__init__.py +2 -0
  38. examples/qwen_vl/collect_data_via_cli.md +423 -0
  39. examples/qwen_vl/collect_vision_traces.py +368 -0
  40. examples/qwen_vl/configs/crafter_rl_vision_qwen3vl4b.toml +127 -0
  41. examples/qwen_vl/configs/crafter_vlm_sft_example.toml +60 -0
  42. examples/qwen_vl/configs/eval_gpt4o_mini_vision.toml +43 -0
  43. examples/qwen_vl/configs/eval_gpt4o_vision_proper.toml +29 -0
  44. examples/qwen_vl/configs/eval_gpt5nano_vision.toml +45 -0
  45. examples/qwen_vl/configs/eval_qwen2vl_vision.toml +44 -0
  46. examples/qwen_vl/configs/filter_qwen2vl_sft.toml +50 -0
  47. examples/qwen_vl/configs/filter_vision_sft.toml +53 -0
  48. examples/qwen_vl/configs/filter_vision_test.toml +8 -0
  49. examples/qwen_vl/configs/sft_qwen3_vl_2b_test.toml +54 -0
  50. examples/qwen_vl/crafter_gpt5nano_agent.py +308 -0
  51. examples/qwen_vl/crafter_qwen_vl_agent.py +300 -0
  52. examples/qwen_vl/run_vision_comparison.sh +62 -0
  53. examples/qwen_vl/run_vision_sft_pipeline.sh +175 -0
  54. examples/qwen_vl/test_image_validation.py +201 -0
  55. examples/qwen_vl/test_sft_vision_data.py +110 -0
  56. examples/rl/README.md +1 -1
  57. examples/rl/configs/eval_base_qwen.toml +17 -0
  58. examples/rl/configs/eval_rl_qwen.toml +13 -0
  59. examples/rl/configs/rl_from_base_qwen.toml +37 -0
  60. examples/rl/configs/rl_from_base_qwen17.toml +76 -0
  61. examples/rl/configs/rl_from_ft_qwen.toml +37 -0
  62. examples/rl/run_eval.py +436 -0
  63. examples/rl/run_rl_and_save.py +111 -0
  64. examples/rl/task_app/README.md +22 -0
  65. examples/rl/task_app/math_single_step.py +990 -0
  66. examples/rl/task_app/math_task_app.py +111 -0
  67. examples/sft/README.md +5 -5
  68. examples/sft/configs/crafter_fft_qwen0p6b.toml +4 -2
  69. examples/sft/configs/crafter_lora_qwen0p6b.toml +4 -3
  70. examples/sft/evaluate.py +4 -4
  71. examples/sft/export_dataset.py +7 -4
  72. examples/sft/generate_traces.py +2 -0
  73. examples/swe/task_app/README.md +1 -1
  74. examples/swe/task_app/grpo_swe_mini.py +1 -1
  75. examples/swe/task_app/grpo_swe_mini_task_app.py +0 -12
  76. examples/swe/task_app/hosted/envs/mini_swe/environment.py +13 -13
  77. examples/swe/task_app/hosted/policy_routes.py +0 -2
  78. examples/swe/task_app/hosted/rollout.py +2 -8
  79. examples/task_apps/IMAGE_ONLY_EVAL_QUICKSTART.md +258 -0
  80. examples/task_apps/crafter/CREATE_SFT_DATASET.md +273 -0
  81. examples/task_apps/crafter/EVAL_IMAGE_ONLY_RESULTS.md +152 -0
  82. examples/task_apps/crafter/FILTER_COMMAND_STATUS.md +174 -0
  83. examples/task_apps/crafter/FILTER_COMMAND_SUCCESS.md +268 -0
  84. examples/task_apps/crafter/QUERY_EXAMPLES.md +203 -0
  85. examples/task_apps/crafter/README_IMAGE_ONLY_EVAL.md +316 -0
  86. examples/task_apps/crafter/eval_image_only_gpt4o.toml +28 -0
  87. examples/task_apps/crafter/eval_text_only_groq_llama.toml +36 -0
  88. examples/task_apps/crafter/filter_sft_dataset.toml +16 -0
  89. examples/task_apps/crafter/task_app/__init__.py +3 -0
  90. examples/task_apps/crafter/task_app/grpo_crafter.py +309 -14
  91. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/environment.py +10 -0
  92. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/policy.py +75 -4
  93. examples/task_apps/crafter/task_app/synth_envs_hosted/envs/crafter/react_agent.py +17 -2
  94. examples/task_apps/crafter/task_app/synth_envs_hosted/inference/openai_client.py +55 -3
  95. examples/task_apps/crafter/task_app/synth_envs_hosted/policy_routes.py +114 -32
  96. examples/task_apps/crafter/task_app/synth_envs_hosted/rollout.py +127 -27
  97. examples/task_apps/crafter/task_app/synth_envs_hosted/utils.py +156 -0
  98. examples/task_apps/enron/__init__.py +1 -0
  99. examples/task_apps/enron/filter_sft.toml +5 -0
  100. examples/task_apps/enron/tests/__init__.py +2 -0
  101. examples/task_apps/enron/tests/integration/__init__.py +2 -0
  102. examples/task_apps/enron/tests/integration/test_enron_eval.py +2 -0
  103. examples/task_apps/enron/tests/unit/__init__.py +2 -0
  104. examples/task_apps/pokemon_red/EVAL_IMAGE_ONLY_COMPLETE.md +283 -0
  105. examples/task_apps/pokemon_red/EVAL_IMAGE_ONLY_STATUS.md +155 -0
  106. examples/task_apps/pokemon_red/README_IMAGE_ONLY_EVAL.md +415 -0
  107. examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +29 -0
  108. examples/task_apps/pokemon_red/pallet_town_rl_config.toml +2 -0
  109. examples/task_apps/pokemon_red/task_app.py +199 -6
  110. examples/task_apps/pokemon_red/test_pallet_town_rewards.py +2 -0
  111. examples/task_apps/sokoban/filter_sft.toml +5 -0
  112. examples/task_apps/sokoban/tests/__init__.py +2 -0
  113. examples/task_apps/sokoban/tests/integration/__init__.py +2 -0
  114. examples/task_apps/sokoban/tests/unit/__init__.py +2 -0
  115. examples/task_apps/verilog/eval_groq_qwen32b.toml +8 -4
  116. examples/task_apps/verilog/filter_sft.toml +5 -0
  117. examples/task_apps/verilog/task_app/grpo_verilog.py +258 -23
  118. examples/task_apps/verilog/tests/__init__.py +2 -0
  119. examples/task_apps/verilog/tests/integration/__init__.py +2 -0
  120. examples/task_apps/verilog/tests/integration/test_verilog_eval.py +2 -0
  121. examples/task_apps/verilog/tests/unit/__init__.py +2 -0
  122. examples/vlm/README.md +3 -3
  123. examples/vlm/configs/crafter_vlm_gpt4o.toml +2 -0
  124. examples/vlm/crafter_openai_vlm_agent.py +3 -5
  125. examples/vlm/filter_image_rows.py +1 -1
  126. examples/vlm/run_crafter_vlm_benchmark.py +2 -2
  127. examples/warming_up_to_rl/_utils.py +92 -0
  128. examples/warming_up_to_rl/analyze_trace_db.py +1 -1
  129. examples/warming_up_to_rl/configs/crafter_fft.toml +2 -0
  130. examples/warming_up_to_rl/configs/crafter_fft_4b.toml +2 -0
  131. examples/warming_up_to_rl/configs/eval_fft_qwen4b.toml +2 -0
  132. examples/warming_up_to_rl/configs/eval_groq_qwen32b.toml +2 -0
  133. examples/warming_up_to_rl/configs/eval_modal_qwen4b.toml +2 -1
  134. examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +2 -1
  135. examples/warming_up_to_rl/configs/rl_from_ft.toml +2 -0
  136. examples/warming_up_to_rl/export_trace_sft.py +174 -60
  137. examples/warming_up_to_rl/groq_test.py +2 -0
  138. examples/warming_up_to_rl/readme.md +63 -132
  139. examples/warming_up_to_rl/run_fft_and_save.py +1 -1
  140. examples/warming_up_to_rl/run_local_rollout.py +2 -0
  141. examples/warming_up_to_rl/run_local_rollout_modal.py +2 -0
  142. examples/warming_up_to_rl/run_local_rollout_parallel.py +2 -0
  143. examples/warming_up_to_rl/run_local_rollout_traced.py +2 -0
  144. examples/warming_up_to_rl/run_rl_and_save.py +1 -1
  145. examples/warming_up_to_rl/run_rollout_remote.py +2 -0
  146. examples/warming_up_to_rl/task_app/README.md +42 -0
  147. examples/warming_up_to_rl/task_app/grpo_crafter.py +696 -0
  148. examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +135 -0
  149. examples/warming_up_to_rl/task_app/synth_envs_hosted/README.md +173 -0
  150. examples/warming_up_to_rl/task_app/synth_envs_hosted/__init__.py +5 -0
  151. examples/warming_up_to_rl/task_app/synth_envs_hosted/branching.py +143 -0
  152. examples/warming_up_to_rl/task_app/synth_envs_hosted/environment_routes.py +1226 -0
  153. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/__init__.py +1 -0
  154. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/__init__.py +6 -0
  155. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/app.py +1 -0
  156. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/environment.py +522 -0
  157. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +478 -0
  158. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +108 -0
  159. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/shared.py +305 -0
  160. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/tools.py +47 -0
  161. examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +204 -0
  162. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/__init__.py +5 -0
  163. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +618 -0
  164. examples/warming_up_to_rl/task_app/synth_envs_hosted/main.py +100 -0
  165. examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +1081 -0
  166. examples/warming_up_to_rl/task_app/synth_envs_hosted/registry.py +195 -0
  167. examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +1861 -0
  168. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/__init__.py +5 -0
  169. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/volume.py +211 -0
  170. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_agents.py +161 -0
  171. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_service.py +137 -0
  172. examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +62 -0
  173. synth_ai/__init__.py +44 -30
  174. synth_ai/_utils/__init__.py +47 -0
  175. synth_ai/_utils/base_url.py +10 -0
  176. synth_ai/_utils/http.py +10 -0
  177. synth_ai/_utils/prompts.py +10 -0
  178. synth_ai/_utils/task_app_state.py +12 -0
  179. synth_ai/_utils/user_config.py +10 -0
  180. synth_ai/api/models/supported.py +145 -7
  181. synth_ai/api/train/__init__.py +13 -1
  182. synth_ai/api/train/cli.py +30 -7
  183. synth_ai/api/train/config_finder.py +18 -11
  184. synth_ai/api/train/env_resolver.py +13 -10
  185. synth_ai/cli/__init__.py +66 -49
  186. synth_ai/cli/_modal_wrapper.py +9 -6
  187. synth_ai/cli/_typer_patch.py +0 -2
  188. synth_ai/cli/_validate_task_app.py +22 -4
  189. synth_ai/cli/legacy_root_backup.py +3 -1
  190. synth_ai/cli/lib/__init__.py +10 -0
  191. synth_ai/cli/lib/task_app_discovery.py +7 -0
  192. synth_ai/cli/lib/task_app_env.py +518 -0
  193. synth_ai/cli/recent.py +1 -0
  194. synth_ai/cli/setup.py +266 -0
  195. synth_ai/cli/task_app_deploy.py +16 -0
  196. synth_ai/cli/task_app_list.py +25 -0
  197. synth_ai/cli/task_app_modal_serve.py +16 -0
  198. synth_ai/cli/task_app_serve.py +18 -0
  199. synth_ai/cli/task_apps.py +392 -141
  200. synth_ai/cli/train.py +18 -0
  201. synth_ai/cli/tui.py +62 -0
  202. synth_ai/demos/__init__.py +10 -0
  203. synth_ai/demos/core/__init__.py +28 -1
  204. synth_ai/demos/crafter/__init__.py +1 -0
  205. synth_ai/demos/crafter/crafter_fft_4b.toml +55 -0
  206. synth_ai/demos/crafter/grpo_crafter_task_app.py +185 -0
  207. synth_ai/demos/crafter/rl_from_base_qwen4b.toml +74 -0
  208. synth_ai/demos/demo_registry.py +176 -0
  209. synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +1 -1
  210. synth_ai/demos/math/__init__.py +1 -0
  211. synth_ai/demos/math/_common.py +16 -0
  212. synth_ai/demos/math/app.py +38 -0
  213. synth_ai/demos/math/config.toml +76 -0
  214. synth_ai/demos/math/deploy_modal.py +54 -0
  215. synth_ai/demos/math/modal_task_app.py +702 -0
  216. synth_ai/demos/math/task_app_entry.py +51 -0
  217. synth_ai/environments/environment/core.py +7 -1
  218. synth_ai/environments/examples/bandit/engine.py +0 -1
  219. synth_ai/environments/examples/bandit/environment.py +0 -1
  220. synth_ai/environments/examples/crafter_classic/environment.py +1 -1
  221. synth_ai/environments/examples/verilog/engine.py +76 -10
  222. synth_ai/environments/examples/wordle/environment.py +0 -1
  223. synth_ai/evals/base.py +16 -5
  224. synth_ai/evals/client.py +1 -1
  225. synth_ai/inference/client.py +1 -1
  226. synth_ai/learning/client.py +1 -1
  227. synth_ai/learning/health.py +1 -1
  228. synth_ai/learning/jobs.py +1 -1
  229. synth_ai/learning/rl/client.py +1 -1
  230. synth_ai/learning/rl/env_keys.py +1 -1
  231. synth_ai/learning/rl/secrets.py +1 -1
  232. synth_ai/learning/sft/client.py +1 -1
  233. synth_ai/learning/sft/data.py +407 -4
  234. synth_ai/learning/validators.py +4 -1
  235. synth_ai/task/__init__.py +11 -1
  236. synth_ai/task/apps/__init__.py +5 -2
  237. synth_ai/task/config.py +259 -0
  238. synth_ai/task/contracts.py +15 -2
  239. synth_ai/task/rubrics/__init__.py +4 -2
  240. synth_ai/task/rubrics/loaders.py +27 -4
  241. synth_ai/task/rubrics/scoring.py +3 -0
  242. synth_ai/task/rubrics.py +219 -0
  243. synth_ai/task/trace_correlation_helpers.py +328 -0
  244. synth_ai/task/tracing_utils.py +14 -3
  245. synth_ai/task/validators.py +145 -2
  246. synth_ai/tracing_v3/config.py +15 -13
  247. synth_ai/tracing_v3/constants.py +21 -0
  248. synth_ai/tracing_v3/db_config.py +3 -1
  249. synth_ai/tracing_v3/decorators.py +10 -7
  250. synth_ai/tracing_v3/session_tracer.py +10 -0
  251. synth_ai/tracing_v3/turso/daemon.py +2 -2
  252. synth_ai/tracing_v3/turso/native_manager.py +108 -77
  253. synth_ai/tracing_v3/utils.py +1 -1
  254. synth_ai/tui/__init__.py +5 -0
  255. synth_ai/tui/__main__.py +13 -0
  256. synth_ai/tui/cli/__init__.py +1 -0
  257. synth_ai/tui/cli/query_experiments.py +164 -0
  258. synth_ai/tui/cli/query_experiments_v3.py +164 -0
  259. synth_ai/tui/dashboard.py +911 -0
  260. synth_ai/utils/__init__.py +101 -0
  261. synth_ai/utils/base_url.py +94 -0
  262. synth_ai/utils/cli.py +131 -0
  263. synth_ai/utils/env.py +287 -0
  264. synth_ai/utils/http.py +169 -0
  265. synth_ai/utils/modal.py +308 -0
  266. synth_ai/utils/process.py +212 -0
  267. synth_ai/utils/prompts.py +39 -0
  268. synth_ai/utils/sqld.py +122 -0
  269. synth_ai/utils/task_app_discovery.py +882 -0
  270. synth_ai/utils/task_app_env.py +186 -0
  271. synth_ai/utils/task_app_state.py +318 -0
  272. synth_ai/utils/user_config.py +137 -0
  273. synth_ai/v0/config/__init__.py +1 -5
  274. synth_ai/v0/config/base_url.py +1 -7
  275. synth_ai/v0/tracing/config.py +1 -1
  276. synth_ai/v0/tracing/decorators.py +1 -1
  277. synth_ai/v0/tracing/upload.py +1 -1
  278. synth_ai/v0/tracing_v1/config.py +1 -1
  279. synth_ai/v0/tracing_v1/decorators.py +1 -1
  280. synth_ai/v0/tracing_v1/upload.py +1 -1
  281. {synth_ai-0.2.13.dev2.dist-info → synth_ai-0.2.16.dist-info}/METADATA +85 -31
  282. {synth_ai-0.2.13.dev2.dist-info → synth_ai-0.2.16.dist-info}/RECORD +286 -135
  283. synth_ai/cli/man.py +0 -106
  284. synth_ai/compound/cais.py +0 -0
  285. synth_ai/core/experiment.py +0 -13
  286. synth_ai/core/system.py +0 -15
  287. synth_ai/demo_registry.py +0 -295
  288. synth_ai/handshake.py +0 -109
  289. synth_ai/http.py +0 -26
  290. {synth_ai-0.2.13.dev2.dist-info → synth_ai-0.2.16.dist-info}/WHEEL +0 -0
  291. {synth_ai-0.2.13.dev2.dist-info → synth_ai-0.2.16.dist-info}/entry_points.txt +0 -0
  292. {synth_ai-0.2.13.dev2.dist-info → synth_ai-0.2.16.dist-info}/licenses/LICENSE +0 -0
  293. {synth_ai-0.2.13.dev2.dist-info → synth_ai-0.2.16.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,328 @@
1
+ """Helpers for trace correlation ID extraction and inclusion in task apps.
2
+
3
+ This module provides utilities for task apps to:
4
+ 1. Extract trace_correlation_id from rollout requests
5
+ 2. Include trace_correlation_id in rollout responses (3 required locations)
6
+
7
+ See monorepo/trace_creation_and_judgement.txt "Fatal Guards" section for requirements.
8
+ """
9
+
10
+ import importlib
11
+ import logging
12
+ from typing import Any, cast
13
+ from urllib.parse import parse_qs, urlparse
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def extract_trace_correlation_id(
19
+ policy_config: dict[str, Any],
20
+ inference_url: str | None = None,
21
+ mode: Any = None
22
+ ) -> str | None:
23
+ """
24
+ Extract trace_correlation_id from policy config or inference URL.
25
+
26
+ This is the standardized method for all task apps to extract the correlation ID
27
+ that the RL trainer generates and passes to the task app.
28
+
29
+ Args:
30
+ policy_config: Policy configuration dict from RolloutRequest.policy.config
31
+ inference_url: Inference URL (optional, used as fallback)
32
+ mode: RolloutMode or string ("rl" or "eval"). Controls warning behavior -
33
+ warnings only logged for RL mode, not EVAL mode.
34
+
35
+ Returns:
36
+ trace_correlation_id if found, None otherwise
37
+
38
+ Extraction order:
39
+ 1. policy_config["trace_correlation_id"] (preferred)
40
+ 2. policy_config["trace"] (legacy fallback)
41
+ 3. URL query param ?cid=... (fallback)
42
+ 4. URL query param ?trace_correlation_id=... (fallback)
43
+ """
44
+ # Try policy_config first (preferred method)
45
+ candidates: list[Any] = [
46
+ policy_config.get("trace_correlation_id"),
47
+ policy_config.get("trace"),
48
+ ]
49
+
50
+ logger.debug(
51
+ "extract_trace_correlation_id: policy_cfg keys=%s candidates=%s",
52
+ sorted(policy_config.keys()),
53
+ candidates,
54
+ )
55
+
56
+ for candidate in candidates:
57
+ if isinstance(candidate, str):
58
+ stripped = candidate.strip()
59
+ if stripped:
60
+ logger.info(
61
+ "extract_trace_correlation_id: extracted from policy_config=%s",
62
+ stripped
63
+ )
64
+ return stripped
65
+
66
+ # Determine if we're in EVAL mode (trace_correlation_id not required for eval)
67
+ rollout_mode_cls: Any | None = None
68
+ try:
69
+ contracts_module = importlib.import_module("synth_ai.task.contracts")
70
+ rollout_mode_cls = getattr(contracts_module, "RolloutMode", None)
71
+ except Exception:
72
+ rollout_mode_cls = None
73
+
74
+ is_eval_mode = False
75
+ if rollout_mode_cls is not None:
76
+ try:
77
+ is_eval_mode = (
78
+ mode == "eval"
79
+ or mode == rollout_mode_cls.EVAL
80
+ or getattr(mode, "value", None) == "eval"
81
+ )
82
+ except Exception:
83
+ is_eval_mode = mode == "eval"
84
+ else:
85
+ is_eval_mode = mode == "eval" or getattr(mode, "value", None) == "eval"
86
+
87
+ # Fallback: try to extract from inference_url query params
88
+ if not inference_url or not isinstance(inference_url, str):
89
+ if is_eval_mode:
90
+ logger.debug(
91
+ "extract_trace_correlation_id: no correlation ID found in policy_config "
92
+ "and no inference_url provided (EVAL mode - expected)"
93
+ )
94
+ else:
95
+ logger.warning(
96
+ "extract_trace_correlation_id: no correlation ID found in policy_config "
97
+ "and no inference_url provided"
98
+ )
99
+ return None
100
+
101
+ try:
102
+ parsed = urlparse(inference_url)
103
+ query_params = cast(dict[str, list[str]], parse_qs(parsed.query or ""))
104
+ # Try multiple possible query param names
105
+ for param_name in ["cid", "trace_correlation_id", "trace"]:
106
+ values = query_params.get(param_name)
107
+ if not values:
108
+ continue
109
+ for value in values:
110
+ if isinstance(value, str) and value.strip():
111
+ correlation_id = value.strip()
112
+ logger.info(
113
+ "extract_trace_correlation_id: extracted from URL param %s=%s",
114
+ param_name,
115
+ correlation_id,
116
+ )
117
+ return correlation_id
118
+ except Exception as e:
119
+ logger.warning(
120
+ "extract_trace_correlation_id: failed to parse inference_url=%s error=%s",
121
+ inference_url,
122
+ e,
123
+ )
124
+
125
+ if is_eval_mode:
126
+ logger.debug(
127
+ "extract_trace_correlation_id: no trace_correlation_id found in "
128
+ "policy_config or inference_url=%s (EVAL mode - expected)",
129
+ inference_url,
130
+ )
131
+ else:
132
+ logger.warning(
133
+ "extract_trace_correlation_id: no trace_correlation_id found in "
134
+ "policy_config or inference_url=%s",
135
+ inference_url,
136
+ )
137
+ return None
138
+
139
+
140
+ def validate_trace_correlation_id(
141
+ trace_correlation_id: str | None,
142
+ run_id: str,
143
+ policy_config: dict[str, Any],
144
+ fatal: bool = False
145
+ ) -> str | None:
146
+ """
147
+ Validate that trace_correlation_id was successfully extracted.
148
+
149
+ Args:
150
+ trace_correlation_id: The extracted correlation ID (or None)
151
+ run_id: Rollout run_id for logging
152
+ policy_config: Policy configuration for debugging
153
+ fatal: If True, raise ValueError on missing ID. If False, log error only.
154
+
155
+ Returns:
156
+ trace_correlation_id if present, None if missing (when fatal=False)
157
+
158
+ Raises:
159
+ ValueError: If trace_correlation_id is missing and fatal=True
160
+ """
161
+ if not trace_correlation_id:
162
+ error_msg = (
163
+ f"🚨 CRITICAL: Cannot extract trace_correlation_id!\n"
164
+ "\n"
165
+ f"Run ID: {run_id}\n"
166
+ f"Policy config keys: {sorted(policy_config.keys())}\n"
167
+ f"Inference URL: {policy_config.get('inference_url', 'NOT_SET')}\n"
168
+ "\n"
169
+ "Checked:\n"
170
+ f"1. policy_config['trace_correlation_id']: {policy_config.get('trace_correlation_id')}\n"
171
+ f"2. policy_config['trace']: {policy_config.get('trace')}\n"
172
+ f"3. inference_url query params\n"
173
+ "\n"
174
+ "Task app CANNOT proceed without trace_correlation_id.\n"
175
+ "This indicates the RL trainer is not sending it correctly.\n"
176
+ "\n"
177
+ "See monorepo/trace_creation_and_judgement.txt 'Fatal Guards' section.\n"
178
+ )
179
+
180
+ if fatal:
181
+ raise ValueError(error_msg)
182
+ else:
183
+ logger.error(error_msg)
184
+
185
+ return trace_correlation_id
186
+
187
+
188
+ def include_trace_correlation_id_in_response(
189
+ response_data: dict[str, Any],
190
+ trace_correlation_id: str | None,
191
+ run_id: str
192
+ ) -> dict[str, Any]:
193
+ """
194
+ Include trace_correlation_id in all required locations of rollout response.
195
+
196
+ Required locations (per Fatal Guards section):
197
+ 1. Top-level response["trace_correlation_id"]
198
+ 2. response["pipeline_metadata"]["trace_correlation_id"]
199
+ 3. Each trajectory["trace_correlation_id"]
200
+
201
+ Args:
202
+ response_data: RolloutResponse dict (from .model_dump())
203
+ trace_correlation_id: The correlation ID to include
204
+ run_id: Rollout run_id for logging
205
+
206
+ Returns:
207
+ Modified response_data with trace_correlation_id in all required places
208
+ """
209
+ if not trace_correlation_id:
210
+ logger.error(
211
+ "include_trace_correlation_id_in_response: missing trace_correlation_id "
212
+ "for run_id=%s - cannot include in response",
213
+ run_id
214
+ )
215
+ return response_data
216
+
217
+ # 1. Add to top-level (REQUIRED)
218
+ if "trace_correlation_id" not in response_data:
219
+ response_data["trace_correlation_id"] = trace_correlation_id
220
+ logger.info(
221
+ "include_trace_correlation_id: added to top-level run_id=%s cid=%s",
222
+ run_id,
223
+ trace_correlation_id
224
+ )
225
+
226
+ # 2. Add to pipeline_metadata (REQUIRED)
227
+ pipeline_meta = response_data.get("pipeline_metadata")
228
+ if not isinstance(pipeline_meta, dict):
229
+ pipeline_meta = {}
230
+ response_data["pipeline_metadata"] = pipeline_meta
231
+
232
+ if "trace_correlation_id" not in pipeline_meta:
233
+ pipeline_meta["trace_correlation_id"] = trace_correlation_id
234
+ logger.info(
235
+ "include_trace_correlation_id: added to pipeline_metadata run_id=%s cid=%s",
236
+ run_id,
237
+ trace_correlation_id
238
+ )
239
+
240
+ # 3. Add to each trajectory (REQUIRED)
241
+ trajectories = response_data.get("trajectories", [])
242
+ if isinstance(trajectories, list):
243
+ for idx, traj in enumerate(trajectories):
244
+ if isinstance(traj, dict) and "trace_correlation_id" not in traj:
245
+ traj["trace_correlation_id"] = trace_correlation_id
246
+ logger.debug(
247
+ "include_trace_correlation_id: added to trajectory[%d] run_id=%s cid=%s",
248
+ idx,
249
+ run_id,
250
+ trace_correlation_id
251
+ )
252
+
253
+ logger.info(
254
+ "include_trace_correlation_id: completed run_id=%s cid=%s "
255
+ "added to %d locations (top-level, metadata, %d trajectories)",
256
+ run_id,
257
+ trace_correlation_id,
258
+ 2 + len(trajectories),
259
+ len(trajectories)
260
+ )
261
+
262
+ return response_data
263
+
264
+
265
+ def verify_trace_correlation_id_in_response(
266
+ response_data: dict[str, Any],
267
+ expected_correlation_id: str | None,
268
+ run_id: str
269
+ ) -> bool:
270
+ """
271
+ Verify that trace_correlation_id is present in all required locations.
272
+
273
+ Args:
274
+ response_data: RolloutResponse dict to verify
275
+ expected_correlation_id: The correlation ID that should be present
276
+ run_id: Rollout run_id for logging
277
+
278
+ Returns:
279
+ True if all required locations have the correlation ID, False otherwise
280
+ """
281
+ if not expected_correlation_id:
282
+ logger.error(
283
+ "verify_trace_correlation_id: no expected_correlation_id provided for run_id=%s",
284
+ run_id
285
+ )
286
+ return False
287
+
288
+ errors = []
289
+
290
+ # Check top-level
291
+ if response_data.get("trace_correlation_id") != expected_correlation_id:
292
+ errors.append(
293
+ f"Top-level missing or mismatch: "
294
+ f"expected={expected_correlation_id} actual={response_data.get('trace_correlation_id')}"
295
+ )
296
+
297
+ # Check pipeline_metadata
298
+ pipeline_meta = response_data.get("pipeline_metadata", {})
299
+ if not isinstance(pipeline_meta, dict) or pipeline_meta.get("trace_correlation_id") != expected_correlation_id:
300
+ errors.append(
301
+ f"pipeline_metadata missing or mismatch: "
302
+ f"expected={expected_correlation_id} actual={pipeline_meta.get('trace_correlation_id') if isinstance(pipeline_meta, dict) else 'NOT_A_DICT'}"
303
+ )
304
+
305
+ # Check trajectories
306
+ trajectories = response_data.get("trajectories", [])
307
+ if isinstance(trajectories, list):
308
+ for idx, traj in enumerate(trajectories):
309
+ if isinstance(traj, dict) and traj.get("trace_correlation_id") != expected_correlation_id:
310
+ errors.append(
311
+ f"trajectory[{idx}] missing or mismatch: "
312
+ f"expected={expected_correlation_id} actual={traj.get('trace_correlation_id')}"
313
+ )
314
+
315
+ if errors:
316
+ logger.error(
317
+ "verify_trace_correlation_id: FAILED run_id=%s\n%s",
318
+ run_id,
319
+ "\n".join(errors)
320
+ )
321
+ return False
322
+
323
+ logger.info(
324
+ "verify_trace_correlation_id: PASSED run_id=%s cid=%s",
325
+ run_id,
326
+ expected_correlation_id
327
+ )
328
+ return True
@@ -4,9 +4,12 @@ from __future__ import annotations
4
4
 
5
5
  import os
6
6
  from collections.abc import Callable
7
+ from datetime import datetime
7
8
  from pathlib import Path
8
9
  from typing import Any
9
10
 
11
+ from synth_ai.tracing_v3.constants import TRACE_DB_DIR, canonical_trace_db_name
12
+
10
13
 
11
14
  def tracing_env_enabled(default: bool = False) -> bool:
12
15
  """Return True when tracing is enabled for task apps via environment variable."""
@@ -40,9 +43,17 @@ def resolve_tracing_db_url() -> str | None:
40
43
  path.parent.mkdir(parents=True, exist_ok=True)
41
44
  return f"sqlite+aiosqlite:///{path}"
42
45
 
43
- fallback_path = Path("traces/v3/synth_ai.db").expanduser()
44
- fallback_path.parent.mkdir(parents=True, exist_ok=True)
45
- return f"sqlite+aiosqlite:///{fallback_path}"
46
+ existing = os.getenv("TASKAPP_TRACE_DB_PATH")
47
+ if existing:
48
+ path = Path(existing).expanduser()
49
+ else:
50
+ base_dir = TRACE_DB_DIR.expanduser()
51
+ base_dir.mkdir(parents=True, exist_ok=True)
52
+ path = base_dir / canonical_trace_db_name(timestamp=datetime.now())
53
+ os.environ["TASKAPP_TRACE_DB_PATH"] = str(path)
54
+ os.environ.setdefault("SQLD_DB_PATH", str(path))
55
+ path.parent.mkdir(parents=True, exist_ok=True)
56
+ return f"sqlite+aiosqlite:///{path}"
46
57
 
47
58
 
48
59
  def build_tracer_factory(
@@ -3,14 +3,157 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import re
6
- from typing import Any
6
+ from typing import Any, cast
7
+ from urllib.parse import urlparse, urlunparse
7
8
 
8
9
  import click
9
10
  import httpx
10
-
11
11
  from synth_ai.task.contracts import TaskAppEndpoints # type: ignore[attr-defined]
12
12
 
13
13
 
14
+ def validate_rollout_response_for_rl(response_data: dict[str, Any], *, warn_only: bool = False) -> list[str]:
15
+ """Validate that a task app rollout response has required fields for RL training.
16
+
17
+ The backend RL trainer requires:
18
+ 1. pipeline_metadata["inference_url"] at top level (with ?cid= for trace correlation)
19
+ 2. Each step's info.meta["inference_url"] must be present (nested structure!)
20
+
21
+ Args:
22
+ response_data: The rollout response dict from task app
23
+ warn_only: If True, return warnings instead of raising exceptions
24
+
25
+ Returns:
26
+ List of validation warnings/errors
27
+
28
+ Raises:
29
+ ValueError: If critical fields are missing (unless warn_only=True)
30
+ """
31
+ issues = []
32
+
33
+ # Check pipeline_metadata
34
+ pipeline_metadata = response_data.get("pipeline_metadata")
35
+ if not isinstance(pipeline_metadata, dict):
36
+ issues.append("Missing or invalid 'pipeline_metadata' (required for RL training)")
37
+ else:
38
+ inference_url = pipeline_metadata.get("inference_url")
39
+ if not inference_url:
40
+ issues.append(
41
+ "pipeline_metadata['inference_url'] is missing. "
42
+ "RL trainer requires this field to extract traces."
43
+ )
44
+ elif not isinstance(inference_url, str):
45
+ issues.append(
46
+ f"pipeline_metadata['inference_url'] must be a string, got: {type(inference_url).__name__}"
47
+ )
48
+ elif "?cid=" not in inference_url:
49
+ issues.append(
50
+ f"pipeline_metadata['inference_url'] should contain '?cid=' for trace correlation. "
51
+ f"Got: {inference_url[:80]}..."
52
+ )
53
+
54
+ # Check trajectories and steps
55
+ trajectories = response_data.get("trajectories", [])
56
+ if not trajectories:
57
+ issues.append("No trajectories found in response")
58
+
59
+ for traj_idx, trajectory in enumerate(trajectories):
60
+ if not isinstance(trajectory, dict):
61
+ continue
62
+
63
+ steps = trajectory.get("steps", [])
64
+ for step_idx, step in enumerate(steps):
65
+ if not isinstance(step, dict):
66
+ continue
67
+
68
+ step_info = step.get("info", {})
69
+ if not isinstance(step_info, dict):
70
+ issues.append(
71
+ f"trajectory[{traj_idx}].steps[{step_idx}].info is not a dict"
72
+ )
73
+ continue
74
+
75
+ # Check for nested meta.inference_url (backend expects this structure!)
76
+ step_meta = step_info.get("meta", {})
77
+ if not isinstance(step_meta, dict):
78
+ issues.append(
79
+ f"trajectory[{traj_idx}].steps[{step_idx}].info.meta is missing or not a dict. "
80
+ f"RL trainer expects nested structure: info.meta.inference_url"
81
+ )
82
+ continue
83
+
84
+ step_inference_url = step_meta.get("inference_url")
85
+ if not step_inference_url:
86
+ issues.append(
87
+ f"trajectory[{traj_idx}].steps[{step_idx}].info.meta['inference_url'] is missing. "
88
+ f"RL trainer needs this for trace extraction (nested structure required!)"
89
+ )
90
+ elif not isinstance(step_inference_url, str):
91
+ issues.append(
92
+ f"trajectory[{traj_idx}].steps[{step_idx}].info.meta['inference_url'] must be a string, "
93
+ f"got: {type(step_inference_url).__name__}"
94
+ )
95
+
96
+ if issues and not warn_only:
97
+ error_msg = "Task app response validation failed for RL training:\n" + "\n".join(
98
+ f" - {issue}" for issue in issues
99
+ )
100
+ raise ValueError(error_msg)
101
+
102
+ return issues
103
+
104
+
105
+ def normalize_inference_url(url: str | None, *, default: str = "https://api.openai.com/v1/chat/completions") -> str:
106
+ """Normalize an inference URL to include the /v1/chat/completions path.
107
+
108
+ This utility ensures inference URLs have the correct path structure for OpenAI-compatible
109
+ chat completions endpoints, while preserving query parameters (e.g., ?cid=trace_123)
110
+ that may be added for tracing.
111
+
112
+ Args:
113
+ url: The inference URL to normalize (may be None or incomplete)
114
+ default: Default URL to use if url is None/empty
115
+
116
+ Returns:
117
+ Normalized URL with proper path and preserved query parameters
118
+
119
+ Examples:
120
+ >>> normalize_inference_url("https://api.groq.com")
121
+ 'https://api.groq.com/v1/chat/completions'
122
+
123
+ >>> normalize_inference_url("https://modal.host?cid=trace_123")
124
+ 'https://modal.host/v1/chat/completions?cid=trace_123'
125
+
126
+ >>> normalize_inference_url("https://api.openai.com/v1")
127
+ 'https://api.openai.com/v1/chat/completions'
128
+
129
+ >>> normalize_inference_url("https://api.groq.com/openai/v1/chat/completions")
130
+ 'https://api.groq.com/openai/v1/chat/completions'
131
+ """
132
+ candidate = (url or default).strip()
133
+ if not candidate:
134
+ candidate = default
135
+
136
+ # Parse the URL to separate path and query components
137
+ parsed = urlparse(candidate)
138
+
139
+ # Check if path already ends with a completions endpoint
140
+ path = parsed.path.rstrip('/')
141
+ if path.endswith("/v1/chat/completions") or path.endswith("/chat/completions"):
142
+ return candidate
143
+
144
+ # Determine what to append based on existing path
145
+ if path.endswith("/v1"):
146
+ new_path = f"{path}/chat/completions"
147
+ elif path.endswith("/chat"):
148
+ new_path = f"{path}/completions"
149
+ else:
150
+ # Default: append full path
151
+ new_path = f"{path}/v1/chat/completions" if path else "/v1/chat/completions"
152
+
153
+ # Reconstruct URL with new path and original query/fragment
154
+ return cast(str, urlunparse(parsed._replace(path=new_path)))
155
+
156
+
14
157
  def validate_task_app_url(url: str | None) -> str:
15
158
  """Validate and normalize a task app URL.
16
159
 
@@ -3,27 +3,29 @@
3
3
  import os
4
4
  from dataclasses import dataclass
5
5
 
6
+ from synth_ai.tracing_v3.constants import canonical_trace_db_path
7
+
8
+ DEFAULT_DB_FILE = str(canonical_trace_db_path())
9
+
10
+
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}"
17
+
6
18
 
7
19
  @dataclass
8
20
  class TursoConfig:
9
21
  """Configuration for Turso/sqld connection."""
10
22
 
11
23
  # Default values matching serve.sh
12
- DEFAULT_DB_FILE = "traces/v3/synth_ai.db"
24
+ DEFAULT_DB_FILE = DEFAULT_DB_FILE
13
25
  DEFAULT_HTTP_PORT = 8080
14
26
 
15
- # Local embedded database for async SQLAlchemy
16
- # Resolve to the actual SQLite file used by sqld if the base path is a directory
17
- def _resolve_sqlite_db_url() -> str: # type: ignore[no-redef]
18
- base_path = os.path.abspath(os.getenv("SQLD_DB_PATH", "traces/v3/synth_ai.db"))
19
- # If sqld is managing this DB, the real SQLite file lives under dbs/default/data
20
- candidate = os.path.join(base_path, "dbs", "default", "data")
21
- if os.path.isdir(base_path) and os.path.exists(candidate):
22
- return f"sqlite+aiosqlite:///{candidate}"
23
- return f"sqlite+aiosqlite:///{base_path}"
24
-
25
27
  # Use env override if provided; otherwise resolve based on SQLD layout
26
- db_url: str = os.getenv("TURSO_LOCAL_DB_URL", _resolve_sqlite_db_url())
28
+ db_url: str = os.getenv("TURSO_LOCAL_DB_URL", _default_sqlite_url())
27
29
 
28
30
  # Remote database sync configuration
29
31
  sync_url: str = os.getenv("TURSO_DATABASE_URL", "")
@@ -48,7 +50,7 @@ class TursoConfig:
48
50
 
49
51
  # Daemon settings (for local sqld) - match serve.sh defaults
50
52
  sqld_binary: str = os.getenv("SQLD_BINARY", "sqld")
51
- sqld_db_path: str = os.getenv("SQLD_DB_PATH", "traces/v3/synth_ai.db")
53
+ sqld_db_path: str = os.getenv("SQLD_DB_PATH", DEFAULT_DB_FILE)
52
54
  sqld_http_port: int = int(os.getenv("SQLD_HTTP_PORT", "8080"))
53
55
  sqld_idle_shutdown: int = int(os.getenv("SQLD_IDLE_SHUTDOWN", "0")) # 0 = no idle shutdown
54
56
 
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+
6
+ TRACE_DB_DIR = Path("traces")
7
+ TRACE_DB_BASENAME = "task_app_traces"
8
+
9
+
10
+ def canonical_trace_db_name(*, timestamp: datetime | None = None) -> str:
11
+ """Return the canonical trace database filename (with optional timestamp suffix)."""
12
+
13
+ if timestamp is None:
14
+ return f"{TRACE_DB_BASENAME}.db"
15
+ return f"{TRACE_DB_BASENAME}_{timestamp.strftime('%Y-%m-%d_%H-%M-%S')}.db"
16
+
17
+
18
+ def canonical_trace_db_path(*, timestamp: datetime | None = None) -> Path:
19
+ """Return the canonical trace database path within the default trace directory."""
20
+
21
+ return TRACE_DB_DIR / canonical_trace_db_name(timestamp=timestamp)
@@ -7,6 +7,8 @@ import os
7
7
  import shutil
8
8
  from typing import TYPE_CHECKING, Optional
9
9
 
10
+ from synth_ai.tracing_v3.constants import canonical_trace_db_path
11
+
10
12
  if TYPE_CHECKING:
11
13
  from .turso.daemon import SqldDaemon
12
14
 
@@ -17,7 +19,7 @@ class DatabaseConfig:
17
19
  """Centralized database configuration management."""
18
20
 
19
21
  # Default values from serve.sh
20
- DEFAULT_DB_FILE = "traces/v3/synth_ai.db"
22
+ DEFAULT_DB_FILE = str(canonical_trace_db_path())
21
23
  DEFAULT_HTTP_PORT = 8080
22
24
 
23
25
  def __init__(
@@ -29,6 +29,7 @@ import contextvars
29
29
  import functools
30
30
  import time
31
31
  from collections.abc import Awaitable, Callable, Mapping
32
+ from contextvars import Token
32
33
  from typing import Any, TypeVar, cast, overload
33
34
 
34
35
  from .abstractions import LMCAISEvent, TimeRecord
@@ -367,11 +368,11 @@ class SessionContext:
367
368
  ```
368
369
  """
369
370
 
370
- def __init__(self, session_id: str, tracer=None):
371
+ def __init__(self, session_id: str, tracer: Any | None = None):
371
372
  self.session_id = session_id
372
373
  self.tracer = tracer
373
- self._token = None
374
- self._tracer_token = None
374
+ self._token: Token[str | None] | None = None
375
+ self._tracer_token: Token[Any] | None = None
375
376
 
376
377
  def __enter__(self):
377
378
  # Store tokens to restore previous context on exit
@@ -382,8 +383,9 @@ class SessionContext:
382
383
 
383
384
  def __exit__(self, exc_type, exc_val, exc_tb):
384
385
  # Restore previous context - this is crucial for proper isolation
385
- _session_id_ctx.reset(self._token)
386
- if self._tracer_token:
386
+ if self._token is not None:
387
+ _session_id_ctx.reset(self._token)
388
+ if self._tracer_token is not None:
387
389
  _session_tracer_ctx.reset(self._tracer_token)
388
390
 
389
391
  async def __aenter__(self):
@@ -393,6 +395,7 @@ class SessionContext:
393
395
  return self
394
396
 
395
397
  async def __aexit__(self, exc_type, exc_val, exc_tb):
396
- _session_id_ctx.reset(self._token)
397
- if self._tracer_token:
398
+ if self._token is not None:
399
+ _session_id_ctx.reset(self._token)
400
+ if self._tracer_token is not None:
398
401
  _session_tracer_ctx.reset(self._tracer_token)
@@ -375,11 +375,21 @@ class SessionTracer:
375
375
 
376
376
  # Save if requested
377
377
  should_save = save if save is not None else self.auto_save
378
+
379
+ # Debug logging
380
+ import logging
381
+ _logger = logging.getLogger(__name__)
382
+ _logger.info(f"[TRACE_DEBUG] end_session: should_save={should_save}, self.db={self.db is not None}, auto_save={self.auto_save}")
383
+
378
384
  if should_save and self.db:
385
+ _logger.info(f"[TRACE_DEBUG] Calling insert_session_trace with {len(self._current_trace.markov_blanket_message_history)} messages")
379
386
  await self.db.insert_session_trace(self._current_trace)
387
+ _logger.info("[TRACE_DEBUG] insert_session_trace completed")
380
388
 
381
389
  # Trigger post-save hooks
382
390
  await self.hooks.trigger("after_save", session=self._current_trace)
391
+ else:
392
+ _logger.warning(f"[TRACE_DEBUG] Skipping save: should_save={should_save}, self.db={self.db is not None}")
383
393
 
384
394
  # Trigger session end hooks
385
395
  await self.hooks.trigger("session_end", session=self._current_trace)