synth-ai 0.2.9.dev7__py3-none-any.whl → 0.2.10__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 (323) hide show
  1. examples/__init__.py +16 -0
  2. examples/crafter_debug_render.py +8 -11
  3. examples/dev/qwen3_32b_qlora_4xh100.toml +40 -0
  4. examples/multi_step/crafter_rl_lora.md +29 -0
  5. examples/qwen_coder/README.md +102 -0
  6. examples/qwen_coder/_shared.py +113 -0
  7. examples/qwen_coder/configs/coder_lora_30b.toml +61 -0
  8. examples/qwen_coder/configs/coder_lora_4b.toml +57 -0
  9. examples/qwen_coder/configs/coder_lora_small.toml +58 -0
  10. examples/qwen_coder/generate_dataset.py +98 -0
  11. examples/qwen_coder/infer_ft_smoke.py +65 -0
  12. examples/qwen_coder/infer_prod_proxy.py +73 -0
  13. examples/qwen_coder/infer_via_synth.py +87 -0
  14. examples/qwen_coder/scripts/infer_coder.sh +19 -0
  15. examples/qwen_coder/scripts/train_coder_30b.sh +22 -0
  16. examples/qwen_coder/sft_full_17b.py +103 -0
  17. examples/qwen_coder/sft_lora_30b.py +110 -0
  18. examples/qwen_coder/subset_jsonl.py +39 -0
  19. examples/qwen_coder/todos.md +38 -0
  20. examples/qwen_coder/validate_jsonl.py +60 -0
  21. examples/rl/run_eval.py +36 -37
  22. examples/rl/run_rl_and_save.py +5 -5
  23. examples/rl/task_app/math_single_step.py +65 -43
  24. examples/rl/task_app/math_task_app.py +3 -3
  25. examples/sft/README.md +139 -0
  26. examples/sft/configs/crafter_fft_qwen0p6b.toml +44 -0
  27. examples/sft/configs/crafter_lora_qwen0p6b.toml +45 -0
  28. examples/sft/evaluate.py +117 -0
  29. examples/sft/export_dataset.py +117 -0
  30. examples/sft/generate_traces.py +162 -0
  31. examples/swe/__init__.py +12 -0
  32. examples/swe/task_app/README.md +105 -0
  33. examples/swe/task_app/__init__.py +2 -0
  34. examples/swe/task_app/grpo_swe_mini.py +571 -0
  35. examples/swe/task_app/grpo_swe_mini_task_app.py +136 -0
  36. examples/swe/task_app/hosted/README.md +173 -0
  37. examples/swe/task_app/hosted/__init__.py +5 -0
  38. examples/swe/task_app/hosted/branching.py +143 -0
  39. examples/swe/task_app/hosted/environment_routes.py +1289 -0
  40. examples/swe/task_app/hosted/envs/__init__.py +1 -0
  41. examples/swe/task_app/hosted/envs/crafter/__init__.py +6 -0
  42. examples/swe/task_app/hosted/envs/crafter/app.py +1 -0
  43. examples/swe/task_app/hosted/envs/crafter/environment.py +522 -0
  44. examples/swe/task_app/hosted/envs/crafter/policy.py +478 -0
  45. examples/swe/task_app/hosted/envs/crafter/react_agent.py +108 -0
  46. examples/swe/task_app/hosted/envs/crafter/shared.py +305 -0
  47. examples/swe/task_app/hosted/envs/crafter/tools.py +47 -0
  48. examples/swe/task_app/hosted/envs/mini_swe/__init__.py +8 -0
  49. examples/swe/task_app/hosted/envs/mini_swe/environment.py +1164 -0
  50. examples/swe/task_app/hosted/envs/mini_swe/policy.py +355 -0
  51. examples/swe/task_app/hosted/envs/mini_swe/shared.py +83 -0
  52. examples/swe/task_app/hosted/envs/mini_swe/tools.py +96 -0
  53. examples/swe/task_app/hosted/hosted_app.py +204 -0
  54. examples/swe/task_app/hosted/inference/__init__.py +5 -0
  55. examples/swe/task_app/hosted/inference/openai_client.py +618 -0
  56. examples/swe/task_app/hosted/main.py +100 -0
  57. examples/swe/task_app/hosted/policy_routes.py +1079 -0
  58. examples/swe/task_app/hosted/registry.py +195 -0
  59. examples/swe/task_app/hosted/rollout.py +1869 -0
  60. examples/swe/task_app/hosted/storage/__init__.py +5 -0
  61. examples/swe/task_app/hosted/storage/volume.py +211 -0
  62. examples/swe/task_app/hosted/test_agents.py +161 -0
  63. examples/swe/task_app/hosted/test_service.py +137 -0
  64. examples/swe/task_app/hosted/utils.py +62 -0
  65. examples/vlm/PROPOSAL.md +53 -0
  66. examples/vlm/README.md +68 -0
  67. examples/vlm/configs/crafter_vlm_gpt4o.toml +44 -0
  68. examples/vlm/crafter_image_only_agent.py +207 -0
  69. examples/vlm/crafter_openai_vlm_agent.py +277 -0
  70. examples/vlm/filter_image_rows.py +63 -0
  71. examples/vlm/run_crafter_vlm_benchmark.py +316 -0
  72. examples/warming_up_to_rl/analyze_trace_db.py +5 -5
  73. examples/warming_up_to_rl/configs/rl_from_base_qwen4b.toml +11 -1
  74. examples/warming_up_to_rl/export_trace_sft.py +78 -21
  75. examples/warming_up_to_rl/groq_test.py +4 -4
  76. examples/warming_up_to_rl/manage_secrets.py +13 -18
  77. examples/warming_up_to_rl/run_eval.py +42 -44
  78. examples/warming_up_to_rl/run_fft_and_save.py +11 -16
  79. examples/warming_up_to_rl/run_local_rollout.py +1 -3
  80. examples/warming_up_to_rl/run_local_rollout_modal.py +2 -4
  81. examples/warming_up_to_rl/run_local_rollout_parallel.py +1 -4
  82. examples/warming_up_to_rl/run_local_rollout_traced.py +3 -5
  83. examples/warming_up_to_rl/run_rl_and_save.py +5 -6
  84. examples/warming_up_to_rl/run_rollout_remote.py +8 -10
  85. examples/warming_up_to_rl/task_app/README.md +6 -2
  86. examples/warming_up_to_rl/task_app/grpo_crafter.py +234 -35
  87. examples/warming_up_to_rl/task_app/grpo_crafter_task_app.py +2 -3
  88. examples/warming_up_to_rl/task_app/synth_envs_hosted/__init__.py +1 -1
  89. examples/warming_up_to_rl/task_app/synth_envs_hosted/branching.py +9 -11
  90. examples/warming_up_to_rl/task_app/synth_envs_hosted/environment_routes.py +131 -114
  91. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/environment.py +101 -41
  92. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/policy.py +73 -51
  93. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/react_agent.py +14 -6
  94. examples/warming_up_to_rl/task_app/synth_envs_hosted/envs/crafter/shared.py +16 -16
  95. examples/warming_up_to_rl/task_app/synth_envs_hosted/hosted_app.py +32 -34
  96. examples/warming_up_to_rl/task_app/synth_envs_hosted/inference/openai_client.py +94 -31
  97. examples/warming_up_to_rl/task_app/synth_envs_hosted/main.py +0 -2
  98. examples/warming_up_to_rl/task_app/synth_envs_hosted/policy_routes.py +303 -203
  99. examples/warming_up_to_rl/task_app/synth_envs_hosted/registry.py +21 -23
  100. examples/warming_up_to_rl/task_app/synth_envs_hosted/rollout.py +328 -225
  101. examples/warming_up_to_rl/task_app/synth_envs_hosted/storage/volume.py +13 -13
  102. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_agents.py +1 -0
  103. examples/warming_up_to_rl/task_app/synth_envs_hosted/test_service.py +1 -0
  104. examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +4 -3
  105. synth_ai/api/models/supported.py +376 -0
  106. synth_ai/api/train/builders.py +128 -21
  107. synth_ai/api/train/cli.py +80 -64
  108. synth_ai/api/train/config_finder.py +7 -2
  109. synth_ai/api/train/env_resolver.py +1 -1
  110. synth_ai/api/train/pollers.py +2 -1
  111. synth_ai/api/train/supported_algos.py +139 -0
  112. synth_ai/api/train/task_app.py +1 -2
  113. synth_ai/api/train/utils.py +13 -44
  114. synth_ai/cli/__init__.py +8 -0
  115. synth_ai/cli/_modal_wrapper.py +28 -0
  116. synth_ai/cli/_typer_patch.py +49 -0
  117. synth_ai/cli/balance.py +1 -2
  118. synth_ai/cli/calc.py +1 -1
  119. synth_ai/cli/demo.py +2 -1
  120. synth_ai/cli/recent.py +2 -2
  121. synth_ai/cli/rl_demo.py +2 -1
  122. synth_ai/cli/root.py +11 -13
  123. synth_ai/cli/status.py +2 -2
  124. synth_ai/cli/task_apps.py +529 -179
  125. synth_ai/cli/traces.py +6 -4
  126. synth_ai/cli/watch.py +12 -18
  127. synth_ai/demo_registry.py +1 -1
  128. synth_ai/demos/core/cli.py +36 -43
  129. synth_ai/demos/demo_task_apps/__init__.py +3 -3
  130. synth_ai/demos/demo_task_apps/core.py +17 -25
  131. synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +3 -4
  132. synth_ai/demos/demo_task_apps/math/app.py +2 -1
  133. synth_ai/demos/demo_task_apps/math/deploy_modal.py +3 -4
  134. synth_ai/demos/demo_task_apps/math/modal_task_app.py +16 -18
  135. synth_ai/demos/demo_task_apps/math/task_app_entry.py +0 -1
  136. synth_ai/environments/examples/crafter_classic/environment.py +76 -1
  137. synth_ai/environments/reproducibility/tree.py +2 -5
  138. synth_ai/environments/service/app.py +11 -12
  139. synth_ai/environments/service/core_routes.py +4 -7
  140. synth_ai/environments/stateful/engine.py +1 -1
  141. synth_ai/environments/tasks/core.py +1 -0
  142. synth_ai/environments/tasks/filters.py +5 -6
  143. synth_ai/environments/tasks/utils.py +4 -5
  144. synth_ai/handshake.py +9 -9
  145. synth_ai/http.py +1 -1
  146. synth_ai/http_client.py +18 -10
  147. synth_ai/inference/client.py +15 -5
  148. synth_ai/jobs/client.py +78 -83
  149. synth_ai/learning/__init__.py +41 -6
  150. synth_ai/learning/algorithms.py +14 -0
  151. synth_ai/learning/client.py +91 -24
  152. synth_ai/learning/config.py +2 -38
  153. synth_ai/learning/ft_client.py +4 -59
  154. synth_ai/learning/health.py +5 -6
  155. synth_ai/learning/jobs.py +31 -47
  156. synth_ai/{rl → learning/rl}/__init__.py +14 -4
  157. synth_ai/learning/rl/client.py +267 -0
  158. synth_ai/learning/rl/config.py +31 -0
  159. synth_ai/{rl → learning/rl}/contracts.py +5 -8
  160. synth_ai/{rl → learning/rl}/env_keys.py +39 -15
  161. synth_ai/learning/rl/secrets.py +13 -0
  162. synth_ai/learning/rl_client.py +2 -281
  163. synth_ai/learning/sft/__init__.py +29 -0
  164. synth_ai/learning/sft/client.py +68 -0
  165. synth_ai/learning/sft/config.py +270 -0
  166. synth_ai/learning/sft/data.py +295 -0
  167. synth_ai/learning/sse.py +25 -24
  168. synth_ai/learning/validators.py +25 -28
  169. synth_ai/lm/__init__.py +21 -47
  170. synth_ai/task/__init__.py +25 -27
  171. synth_ai/task/apps/__init__.py +7 -8
  172. synth_ai/task/auth.py +8 -8
  173. synth_ai/task/client.py +14 -14
  174. synth_ai/task/contracts.py +36 -35
  175. synth_ai/task/datasets.py +6 -5
  176. synth_ai/task/errors.py +10 -10
  177. synth_ai/task/health.py +17 -9
  178. synth_ai/task/json.py +58 -23
  179. synth_ai/task/proxy.py +13 -9
  180. synth_ai/task/rubrics.py +16 -15
  181. synth_ai/task/server.py +12 -12
  182. synth_ai/task/tracing_utils.py +4 -4
  183. synth_ai/task/vendors.py +5 -6
  184. synth_ai/tracing_v3/__init__.py +2 -0
  185. synth_ai/tracing_v3/abstractions.py +21 -4
  186. synth_ai/tracing_v3/decorators.py +18 -16
  187. synth_ai/tracing_v3/hooks.py +5 -5
  188. synth_ai/tracing_v3/llm_call_record_helpers.py +6 -6
  189. synth_ai/tracing_v3/session_tracer.py +40 -14
  190. synth_ai/tracing_v3/storage/base.py +85 -0
  191. synth_ai/tracing_v3/storage/config.py +21 -8
  192. synth_ai/tracing_v3/storage/factory.py +10 -7
  193. synth_ai/tracing_v3/storage/utils.py +4 -2
  194. synth_ai/tracing_v3/turso/daemon.py +7 -2
  195. synth_ai/tracing_v3/turso/models.py +2 -2
  196. synth_ai/tracing_v3/turso/native_manager.py +1173 -0
  197. synth_ai/tracing_v3/utils.py +4 -4
  198. synth_ai/v0/api/__init__.py +8 -0
  199. synth_ai/v0/api/models/__init__.py +8 -0
  200. synth_ai/v0/api/models/supported.py +8 -0
  201. synth_ai/v0/config/__init__.py +15 -0
  202. synth_ai/v0/config/base_url.py +12 -0
  203. synth_ai/v0/lm/__init__.py +51 -0
  204. synth_ai/{lm → v0/lm}/caching/ephemeral.py +2 -2
  205. synth_ai/{lm → v0/lm}/caching/handler.py +4 -4
  206. synth_ai/{lm → v0/lm}/caching/initialize.py +1 -1
  207. synth_ai/{lm → v0/lm}/caching/persistent.py +1 -1
  208. synth_ai/{lm → v0/lm}/config.py +6 -1
  209. synth_ai/{lm → v0/lm}/core/all.py +9 -9
  210. synth_ai/{lm → v0/lm}/core/main.py +6 -6
  211. synth_ai/{lm → v0/lm}/core/main_v3.py +10 -10
  212. synth_ai/{lm → v0/lm}/core/synth_models.py +2 -14
  213. synth_ai/{lm → v0/lm}/core/vendor_clients.py +2 -2
  214. synth_ai/{lm → v0/lm}/overrides.py +2 -2
  215. synth_ai/{lm → v0/lm}/provider_support/anthropic.py +4 -4
  216. synth_ai/{lm → v0/lm}/provider_support/openai.py +5 -5
  217. synth_ai/{lm → v0/lm}/structured_outputs/handler.py +5 -5
  218. synth_ai/{lm → v0/lm}/structured_outputs/rehabilitate.py +1 -1
  219. synth_ai/{lm → v0/lm}/vendors/core/anthropic_api.py +9 -9
  220. synth_ai/{lm → v0/lm}/vendors/core/gemini_api.py +5 -5
  221. synth_ai/{lm → v0/lm}/vendors/core/mistral_api.py +5 -5
  222. synth_ai/{lm → v0/lm}/vendors/core/openai_api.py +10 -10
  223. synth_ai/{lm → v0/lm}/vendors/openai_standard.py +8 -8
  224. synth_ai/{lm → v0/lm}/vendors/openai_standard_responses.py +2 -2
  225. synth_ai/{lm → v0/lm}/vendors/supported/custom_endpoint.py +3 -3
  226. synth_ai/{lm → v0/lm}/vendors/supported/deepseek.py +2 -2
  227. synth_ai/{lm → v0/lm}/vendors/supported/grok.py +2 -2
  228. synth_ai/{lm → v0/lm}/vendors/supported/groq.py +1 -1
  229. synth_ai/{lm → v0/lm}/vendors/supported/ollama.py +1 -1
  230. synth_ai/{lm → v0/lm}/vendors/supported/openrouter.py +3 -3
  231. synth_ai/{lm → v0/lm}/vendors/supported/together.py +1 -1
  232. synth_ai/{lm → v0/lm}/vendors/synth_client.py +1 -1
  233. synth_ai/v0/tracing_v3/__init__.py +10 -0
  234. synth_ai/v0/tracing_v3/abstractions.py +3 -0
  235. synth_ai/v0/tracing_v3/decorators.py +3 -0
  236. synth_ai/v0/tracing_v3/llm_call_record_helpers.py +3 -0
  237. synth_ai/v0/tracing_v3/session_tracer.py +3 -0
  238. {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.10.dist-info}/METADATA +10 -7
  239. {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.10.dist-info}/RECORD +269 -233
  240. examples/common_old/backend.py +0 -20
  241. examples/evals_old/README.md +0 -98
  242. examples/evals_old/__init__.py +0 -6
  243. examples/evals_old/compare_models.py +0 -1038
  244. examples/evals_old/example_log.md +0 -145
  245. examples/evals_old/run_demo.sh +0 -126
  246. examples/evals_old/trace_analysis.py +0 -270
  247. examples/finetuning_old/_backup_synth_qwen/config.toml +0 -29
  248. examples/finetuning_old/_backup_synth_qwen/example_log.md +0 -324
  249. examples/finetuning_old/_backup_synth_qwen/filter_traces.py +0 -60
  250. examples/finetuning_old/_backup_synth_qwen/filter_traces_achievements.py +0 -243
  251. examples/finetuning_old/_backup_synth_qwen/purge_v3_traces.py +0 -109
  252. examples/finetuning_old/_backup_synth_qwen/react_agent_lm.py +0 -1924
  253. examples/finetuning_old/_backup_synth_qwen/readme.md +0 -49
  254. examples/finetuning_old/_backup_synth_qwen/run_crafter_qwen4b.py +0 -114
  255. examples/finetuning_old/_backup_synth_qwen/run_demo.sh +0 -195
  256. examples/finetuning_old/_backup_synth_qwen/sft_kickoff.py +0 -119
  257. examples/finetuning_old/synth_qwen_v1/README.md +0 -68
  258. examples/finetuning_old/synth_qwen_v1/filter_traces.py +0 -60
  259. examples/finetuning_old/synth_qwen_v1/filter_traces_achievements.py +0 -243
  260. examples/finetuning_old/synth_qwen_v1/finetune.py +0 -46
  261. examples/finetuning_old/synth_qwen_v1/hello_ft_model.py +0 -71
  262. examples/finetuning_old/synth_qwen_v1/infer.py +0 -36
  263. examples/finetuning_old/synth_qwen_v1/poll.py +0 -46
  264. examples/finetuning_old/synth_qwen_v1/prepare_data.py +0 -35
  265. examples/finetuning_old/synth_qwen_v1/purge_v3_traces.py +0 -109
  266. examples/finetuning_old/synth_qwen_v1/react_agent_lm.py +0 -1933
  267. examples/finetuning_old/synth_qwen_v1/run_crafter_sft_job.py +0 -210
  268. examples/finetuning_old/synth_qwen_v1/run_ft_job.py +0 -237
  269. examples/finetuning_old/synth_qwen_v1/upload_data.py +0 -34
  270. examples/finetuning_old/synth_qwen_v1/util.py +0 -152
  271. examples/rl_old/task_app.py +0 -1131
  272. synth_ai/experimental/synth_oss.py +0 -445
  273. synth_ai/learning/filtering.py +0 -0
  274. synth_ai/learning/offline/dpo.py +0 -0
  275. synth_ai/learning/offline/providers.py +0 -7
  276. synth_ai/learning/offline/sft.py +0 -0
  277. synth_ai/learning/offline/shared.py +0 -0
  278. synth_ai/learning/online/grpo.py +0 -0
  279. synth_ai/learning/online/irft.py +0 -0
  280. synth_ai/learning/prompts/banking77_injection_eval.py +0 -168
  281. synth_ai/learning/prompts/gepa.py +0 -0
  282. synth_ai/learning/prompts/hello_world_in_context_injection_ex.py +0 -211
  283. synth_ai/learning/prompts/mipro.py +0 -289
  284. synth_ai/learning/prompts/random_search.py +0 -249
  285. synth_ai/learning/prompts/run_mipro_banking77.py +0 -172
  286. synth_ai/learning/prompts/run_random_search_banking77.py +0 -329
  287. synth_ai/rl/secrets.py +0 -19
  288. synth_ai/scripts/verify_rewards.py +0 -100
  289. synth_ai/tracing/__init__.py +0 -30
  290. synth_ai/tracing_v1/__init__.py +0 -33
  291. synth_ai/tracing_v3/turso/__init__.py +0 -25
  292. synth_ai/tracing_v3/turso/manager.py +0 -838
  293. synth_ai/zyk/__init__.py +0 -30
  294. /synth_ai/{lm → v0/lm}/caching/__init__.py +0 -0
  295. /synth_ai/{lm → v0/lm}/caching/constants.py +0 -0
  296. /synth_ai/{lm → v0/lm}/caching/dbs.py +0 -0
  297. /synth_ai/{lm → v0/lm}/constants.py +0 -0
  298. /synth_ai/{lm → v0/lm}/core/__init__.py +0 -0
  299. /synth_ai/{lm → v0/lm}/core/exceptions.py +0 -0
  300. /synth_ai/{lm → v0/lm}/cost/__init__.py +0 -0
  301. /synth_ai/{lm → v0/lm}/cost/monitor.py +0 -0
  302. /synth_ai/{lm → v0/lm}/cost/statefulness.py +0 -0
  303. /synth_ai/{lm → v0/lm}/injection.py +0 -0
  304. /synth_ai/{lm → v0/lm}/provider_support/__init__.py +0 -0
  305. /synth_ai/{lm → v0/lm}/provider_support/suppress_logging.py +0 -0
  306. /synth_ai/{lm → v0/lm}/structured_outputs/__init__.py +0 -0
  307. /synth_ai/{lm → v0/lm}/structured_outputs/inject.py +0 -0
  308. /synth_ai/{lm → v0/lm}/tools/__init__.py +0 -0
  309. /synth_ai/{lm → v0/lm}/tools/base.py +0 -0
  310. /synth_ai/{lm → v0/lm}/unified_interface.py +0 -0
  311. /synth_ai/{lm → v0/lm}/vendors/__init__.py +0 -0
  312. /synth_ai/{lm → v0/lm}/vendors/base.py +0 -0
  313. /synth_ai/{lm → v0/lm}/vendors/core/__init__.py +0 -0
  314. /synth_ai/{lm → v0/lm}/vendors/core/synth_dev_api.py +0 -0
  315. /synth_ai/{lm → v0/lm}/vendors/local/__init__.py +0 -0
  316. /synth_ai/{lm → v0/lm}/vendors/local/ollama.py +0 -0
  317. /synth_ai/{lm → v0/lm}/vendors/retries.py +0 -0
  318. /synth_ai/{lm → v0/lm}/vendors/supported/__init__.py +0 -0
  319. /synth_ai/{lm → v0/lm}/warmup.py +0 -0
  320. {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.10.dist-info}/WHEEL +0 -0
  321. {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.10.dist-info}/entry_points.txt +0 -0
  322. {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.10.dist-info}/licenses/LICENSE +0 -0
  323. {synth_ai-0.2.9.dev7.dist-info → synth_ai-0.2.10.dist-info}/top_level.txt +0 -0
synth_ai/task/json.py CHANGED
@@ -1,9 +1,9 @@
1
- from __future__ import annotations
2
-
3
1
  """Shared JSON sanitisation helpers for Task Apps."""
4
2
 
3
+ from __future__ import annotations
4
+
5
5
  from collections.abc import Mapping, Sequence
6
- from dataclasses import is_dataclass, asdict
6
+ from dataclasses import asdict, is_dataclass
7
7
  from enum import Enum
8
8
  from typing import Any
9
9
 
@@ -13,13 +13,19 @@ except Exception: # pragma: no cover - handled at runtime
13
13
  _np = None # type: ignore
14
14
 
15
15
 
16
- def _mask_numpy_array(arr: "_np.ndarray") -> str:
16
+ def _mask_numpy_array(arr: Any) -> str:
17
17
  shape = getattr(arr, "shape", None)
18
18
  dtype = getattr(arr, "dtype", None)
19
19
  return f"<ndarray shape={shape} dtype={dtype}>"
20
20
 
21
21
 
22
- def to_jsonable(value: Any) -> Any:
22
+ def to_jsonable(
23
+ value: Any,
24
+ *,
25
+ _visited: set[int] | None = None,
26
+ _depth: int = 0,
27
+ _max_depth: int = 32,
28
+ ) -> Any:
23
29
  """Convert `value` into structures compatible with JSON serialisation.
24
30
 
25
31
  - numpy scalars are converted to their Python counterparts
@@ -29,25 +35,33 @@ def to_jsonable(value: Any) -> Any:
29
35
  - non-serialisable objects fall back to `repr`
30
36
  """
31
37
 
32
- if value is None or isinstance(value, (str, bool, int, float)):
38
+ if _visited is None:
39
+ _visited = set()
40
+
41
+ if _depth > _max_depth:
42
+ return f"<max_depth type={type(value).__name__}>"
43
+
44
+ if value is None or isinstance(value, str | bool | int | float):
33
45
  return value
34
46
 
35
47
  # numpy scalars / arrays
36
48
  if _np is not None:
37
- if isinstance(value, (_np.integer,)):
49
+ if isinstance(value, _np.integer):
38
50
  return int(value)
39
- if isinstance(value, (_np.floating,)):
51
+ if isinstance(value, _np.floating):
40
52
  return float(value)
41
- if isinstance(value, (_np.bool_,)):
53
+ if isinstance(value, _np.bool_):
42
54
  return bool(value)
43
- if isinstance(value, (_np.ndarray,)):
55
+ if isinstance(value, _np.ndarray):
44
56
  return _mask_numpy_array(value)
45
57
 
46
58
  if isinstance(value, Enum):
47
- return to_jsonable(value.value)
59
+ return to_jsonable(value.value, _visited=_visited, _depth=_depth + 1, _max_depth=_max_depth)
48
60
 
49
61
  if is_dataclass(value):
50
- return to_jsonable(asdict(value))
62
+ return to_jsonable(
63
+ asdict(value), _visited=_visited, _depth=_depth + 1, _max_depth=_max_depth
64
+ )
51
65
 
52
66
  # pydantic BaseModel / attrs objects
53
67
  for attr in ("model_dump", "dict", "to_dict", "to_json"):
@@ -56,21 +70,42 @@ def to_jsonable(value: Any) -> Any:
56
70
  dumped = getattr(value, attr)() # type: ignore[misc]
57
71
  except TypeError:
58
72
  dumped = getattr(value, attr)(exclude_none=False) # pragma: no cover
59
- return to_jsonable(dumped)
60
-
61
- if isinstance(value, Mapping):
62
- return {str(k): to_jsonable(v) for k, v in value.items()}
73
+ return to_jsonable(
74
+ dumped, _visited=_visited, _depth=_depth + 1, _max_depth=_max_depth
75
+ )
63
76
 
64
- if isinstance(value, (set, tuple)):
65
- return [to_jsonable(v) for v in value]
77
+ obj_id = id(value)
78
+ if obj_id in _visited:
79
+ return f"<circular type={type(value).__name__}>"
66
80
 
67
- if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
68
- return [to_jsonable(v) for v in value]
69
-
70
- if isinstance(value, (bytes, bytearray)):
81
+ if isinstance(value, Mapping):
82
+ _visited.add(obj_id)
83
+ return {
84
+ str(k): to_jsonable(v, _visited=_visited, _depth=_depth + 1, _max_depth=_max_depth)
85
+ for k, v in value.items()
86
+ }
87
+
88
+ if isinstance(value, set | tuple):
89
+ _visited.add(obj_id)
90
+ return [
91
+ to_jsonable(v, _visited=_visited, _depth=_depth + 1, _max_depth=_max_depth)
92
+ for v in value
93
+ ]
94
+
95
+ if isinstance(value, Sequence) and not isinstance(value, str | bytes | bytearray):
96
+ _visited.add(obj_id)
97
+ return [
98
+ to_jsonable(v, _visited=_visited, _depth=_depth + 1, _max_depth=_max_depth)
99
+ for v in value
100
+ ]
101
+
102
+ if isinstance(value, bytes | bytearray):
71
103
  return f"<bytes len={len(value)}>"
72
104
 
73
105
  if hasattr(value, "__dict__"):
74
- return to_jsonable(vars(value))
106
+ _visited.add(obj_id)
107
+ return to_jsonable(
108
+ vars(value), _visited=_visited, _depth=_depth + 1, _max_depth=_max_depth
109
+ )
75
110
 
76
111
  return repr(value)
synth_ai/task/proxy.py CHANGED
@@ -1,14 +1,14 @@
1
- from __future__ import annotations
2
-
3
1
  """Shared helpers for Task App proxy endpoints (OpenAI, Groq, etc.)."""
4
2
 
3
+ from __future__ import annotations
4
+
5
5
  import copy
6
6
  import json
7
7
  import re
8
- from typing import Any, Iterable, List, Tuple
9
-
8
+ from collections.abc import Iterable
9
+ from typing import Any
10
10
 
11
- INTERACT_TOOL_SCHEMA: List[dict[str, Any]] = [
11
+ INTERACT_TOOL_SCHEMA: list[dict[str, Any]] = [
12
12
  {
13
13
  "type": "function",
14
14
  "function": {
@@ -80,9 +80,13 @@ def prepare_for_groq(model: str | None, payload: dict[str, Any]) -> dict[str, An
80
80
 
81
81
  sanitized = prepare_for_openai(model, payload)
82
82
  # Groq supports `max_tokens`; prefer their native parameter when present
83
- if model and "gpt-5" not in (model or ""):
84
- if "max_completion_tokens" in sanitized and "max_tokens" not in payload:
85
- sanitized["max_tokens"] = sanitized.pop("max_completion_tokens")
83
+ if (
84
+ model
85
+ and "gpt-5" not in model
86
+ and "max_completion_tokens" in sanitized
87
+ and "max_tokens" not in payload
88
+ ):
89
+ sanitized["max_tokens"] = sanitized.pop("max_completion_tokens")
86
90
  return sanitized
87
91
 
88
92
 
@@ -146,7 +150,7 @@ def _parse_actions_from_json_candidate(candidate: Any) -> tuple[list[str], str]:
146
150
  return actions, reasoning
147
151
 
148
152
 
149
- def parse_tool_call_from_text(text: str) -> Tuple[list[str], str]:
153
+ def parse_tool_call_from_text(text: str) -> tuple[list[str], str]:
150
154
  """Derive tool-call actions and reasoning from assistant text."""
151
155
 
152
156
  text = (text or "").strip()
synth_ai/task/rubrics.py CHANGED
@@ -1,10 +1,11 @@
1
- from __future__ import annotations
2
-
3
1
  """Rubric schema, loading, and scoring helpers for Task Apps."""
4
2
 
3
+ from __future__ import annotations
4
+
5
5
  import json
6
+ from collections.abc import Iterable
6
7
  from pathlib import Path
7
- from typing import Any, Dict, Iterable, Optional
8
+ from typing import Any
8
9
 
9
10
  from pydantic import BaseModel, Field, field_validator
10
11
 
@@ -48,14 +49,14 @@ class Rubric(BaseModel):
48
49
  return criteria
49
50
 
50
51
 
51
- def _load_text(source: str) -> tuple[str, Optional[str]]:
52
+ def _load_text(source: str) -> tuple[str, str | None]:
52
53
  path = Path(source)
53
54
  if path.exists():
54
55
  return path.read_text(encoding="utf-8"), path.suffix.lower()
55
56
  return source, None
56
57
 
57
58
 
58
- def _parse_structured(text: str, suffix: Optional[str]) -> Dict[str, Any]:
59
+ def _parse_structured(text: str, suffix: str | None) -> dict[str, Any]:
59
60
  text = text.strip()
60
61
  if not text:
61
62
  raise ValueError("Rubric source is empty")
@@ -66,7 +67,7 @@ def _parse_structured(text: str, suffix: Optional[str]) -> Dict[str, Any]:
66
67
  raise RuntimeError("PyYAML is required to load YAML rubrics") from exc
67
68
  data = yaml.safe_load(text)
68
69
  if not isinstance(data, dict):
69
- raise ValueError("Rubric YAML must produce a mapping")
70
+ raise ValueError("Rubric YAML must produce a mapping") from None
70
71
  return data
71
72
  if text.startswith("{"):
72
73
  return json.loads(text)
@@ -85,7 +86,7 @@ def _parse_structured(text: str, suffix: Optional[str]) -> Dict[str, Any]:
85
86
  raise RuntimeError("PyYAML is required to load rubric text") from exc
86
87
  data = yaml.safe_load(text)
87
88
  if not isinstance(data, dict):
88
- raise ValueError("Rubric text must decode to a mapping")
89
+ raise ValueError("Rubric text must decode to a mapping") from None
89
90
  return data
90
91
 
91
92
 
@@ -148,7 +149,7 @@ def blend_rubrics(base: Rubric | None, override: Rubric | None) -> Rubric | None
148
149
  )
149
150
 
150
151
 
151
- def _as_float(value: Any) -> Optional[float]:
152
+ def _as_float(value: Any) -> float | None:
152
153
  try:
153
154
  return float(value)
154
155
  except Exception:
@@ -156,11 +157,11 @@ def _as_float(value: Any) -> Optional[float]:
156
157
 
157
158
 
158
159
  def _score(
159
- criteria: Iterable[Criterion], values: Dict[str, float], aggregation: str
160
- ) -> Dict[str, Any]:
160
+ criteria: Iterable[Criterion], values: dict[str, float], aggregation: str
161
+ ) -> dict[str, Any]:
161
162
  if aggregation == "inherit":
162
163
  aggregation = "weighted_sum"
163
- per_criterion: Dict[str, Dict[str, Any]] = {}
164
+ per_criterion: dict[str, dict[str, Any]] = {}
164
165
  total = 0.0
165
166
  total_weight = 0.0
166
167
  for criterion in criteria:
@@ -188,10 +189,10 @@ def _score(
188
189
 
189
190
  def score_events_against_rubric(
190
191
  events: list[dict[str, Any]], rubric: Rubric | None
191
- ) -> Dict[str, Any]:
192
+ ) -> dict[str, Any]:
192
193
  if rubric is None:
193
194
  return {"aggregation": "none", "score": None, "per_criterion": {}}
194
- values: Dict[str, float] = {}
195
+ values: dict[str, float] = {}
195
196
  for event in events or []:
196
197
  if not isinstance(event, dict):
197
198
  continue
@@ -202,10 +203,10 @@ def score_events_against_rubric(
202
203
  return _score(rubric.criteria, values, rubric.aggregation)
203
204
 
204
205
 
205
- def score_outcome_against_rubric(outcome: dict[str, Any], rubric: Rubric | None) -> Dict[str, Any]:
206
+ def score_outcome_against_rubric(outcome: dict[str, Any], rubric: Rubric | None) -> dict[str, Any]:
206
207
  if rubric is None:
207
208
  return {"aggregation": "none", "score": None, "per_criterion": {}}
208
- values: Dict[str, float] = {}
209
+ values: dict[str, float] = {}
209
210
  if isinstance(outcome, dict):
210
211
  candidates = (
211
212
  outcome.get("criteria") if isinstance(outcome.get("criteria"), dict) else outcome
synth_ai/task/server.py CHANGED
@@ -1,39 +1,34 @@
1
- from __future__ import annotations
2
-
3
1
  """FastAPI scaffolding for Task Apps (local dev + deployment)."""
4
2
 
3
+ from __future__ import annotations
4
+
5
5
  import asyncio
6
6
  import inspect
7
7
  import os
8
+ from collections.abc import Awaitable, Callable, Iterable, Mapping, MutableMapping, Sequence
8
9
  from dataclasses import dataclass, field
9
10
  from pathlib import Path
10
- from typing import Any, Awaitable, Callable, Iterable, Mapping, MutableMapping, Sequence
11
+ from typing import Any
11
12
 
12
13
  import httpx
13
14
  from fastapi import APIRouter, Depends, FastAPI, Query, Request
14
15
  from fastapi.middleware.cors import CORSMiddleware
15
- from fastapi.responses import JSONResponse
16
16
  from starlette.middleware import Middleware
17
17
 
18
- from .auth import (
19
- is_api_key_header_authorized,
20
- normalize_environment_api_key,
21
- require_api_key_dependency,
22
- )
18
+ from .auth import normalize_environment_api_key, require_api_key_dependency
23
19
  from .contracts import RolloutRequest, RolloutResponse, TaskInfo
24
20
  from .datasets import TaskDatasetRegistry
25
21
  from .errors import http_exception
26
22
  from .json import to_jsonable
27
23
  from .proxy import (
24
+ inject_system_hint,
28
25
  prepare_for_groq,
29
26
  prepare_for_openai,
30
- inject_system_hint,
31
27
  synthesize_tool_call_if_missing,
32
28
  )
33
29
  from .rubrics import Rubric
34
30
  from .vendors import get_groq_key_or_503, get_openai_key_or_503, normalize_vendor_keys
35
31
 
36
-
37
32
  TasksetDescriptor = Callable[[], Mapping[str, Any] | Awaitable[Mapping[str, Any]]]
38
33
  InstanceProvider = Callable[[Sequence[int]], Iterable[TaskInfo] | Awaitable[Iterable[TaskInfo]]]
39
34
  RolloutExecutor = Callable[[RolloutRequest, Request], Any | Awaitable[Any]]
@@ -81,7 +76,7 @@ class TaskAppConfig:
81
76
  startup_hooks: Sequence[Callable[[], None | Awaitable[None]]] = field(default_factory=tuple)
82
77
  shutdown_hooks: Sequence[Callable[[], None | Awaitable[None]]] = field(default_factory=tuple)
83
78
 
84
- def clone(self) -> "TaskAppConfig":
79
+ def clone(self) -> TaskAppConfig:
85
80
  """Return a shallow copy safe to mutate when wiring the app."""
86
81
 
87
82
  return TaskAppConfig(
@@ -299,6 +294,11 @@ def create_task_app(config: TaskAppConfig) -> FastAPI:
299
294
  }
300
295
  )
301
296
 
297
+ @app.post("/done", dependencies=[Depends(auth_dependency)])
298
+ async def done() -> Mapping[str, Any]:
299
+ # Coordination endpoint for tests and automation; indicates app is reachable
300
+ return to_jsonable({"ok": True, "service": cfg.app_id})
301
+
302
302
  @app.get("/info", dependencies=[Depends(auth_dependency)])
303
303
  async def info() -> Mapping[str, Any]:
304
304
  dataset_meta = cfg.base_task_info.dataset
@@ -1,11 +1,11 @@
1
- from __future__ import annotations
2
-
3
1
  """Utilities for wiring tracing_v3 into task apps."""
4
2
 
3
+ from __future__ import annotations
4
+
5
5
  import os
6
- import time
6
+ from collections.abc import Callable
7
7
  from pathlib import Path
8
- from typing import Any, Callable
8
+ from typing import Any
9
9
 
10
10
 
11
11
  def tracing_env_enabled(default: bool = False) -> bool:
synth_ai/task/vendors.py CHANGED
@@ -1,9 +1,8 @@
1
- from __future__ import annotations
2
-
3
1
  """Vendor API key helpers shared by Task Apps."""
4
2
 
3
+ from __future__ import annotations
4
+
5
5
  import os
6
- from typing import Optional
7
6
 
8
7
  from .errors import http_exception
9
8
 
@@ -20,7 +19,7 @@ def _mask(value: str, *, prefix: int = 4) -> str:
20
19
  return f"{visible}{'…' if len(value) > prefix else ''}"
21
20
 
22
21
 
23
- def _normalize_single(key: str) -> Optional[str]:
22
+ def _normalize_single(key: str) -> str | None:
24
23
  direct = os.getenv(key)
25
24
  if direct:
26
25
  return direct
@@ -37,10 +36,10 @@ def _normalize_single(key: str) -> Optional[str]:
37
36
  return None
38
37
 
39
38
 
40
- def normalize_vendor_keys() -> dict[str, Optional[str]]:
39
+ def normalize_vendor_keys() -> dict[str, str | None]:
41
40
  """Normalise known vendor keys from dev fallbacks and return the mapping."""
42
41
 
43
- resolved: dict[str, Optional[str]] = {}
42
+ resolved: dict[str, str | None] = {}
44
43
  for key in _VENDOR_KEYS:
45
44
  resolved[key] = _normalize_single(key)
46
45
  return resolved
@@ -75,6 +75,7 @@ from .abstractions import (
75
75
  EnvironmentEvent,
76
76
  RuntimeEvent,
77
77
  SessionEventMarkovBlanketMessage,
78
+ SessionMessageContent,
78
79
  SessionTimeStep,
79
80
  SessionTrace,
80
81
  TimeRecord,
@@ -90,6 +91,7 @@ __all__ = [
90
91
  "RuntimeEvent",
91
92
  "EnvironmentEvent",
92
93
  "SessionEventMarkovBlanketMessage",
94
+ "SessionMessageContent",
93
95
  "TimeRecord",
94
96
  "TursoConfig",
95
97
  ]
@@ -37,7 +37,7 @@ Concepts:
37
37
  from __future__ import annotations
38
38
 
39
39
  from dataclasses import asdict, dataclass, field
40
- from datetime import datetime
40
+ from datetime import UTC, datetime
41
41
  from typing import Any
42
42
 
43
43
  from .lm_call_record_abstractions import LLMCallRecord
@@ -61,6 +61,23 @@ class TimeRecord:
61
61
  message_time: int | None = None
62
62
 
63
63
 
64
+ @dataclass(frozen=True)
65
+ class SessionMessageContent:
66
+ """Normalized payload stored alongside session messages."""
67
+
68
+ text: str | None = None
69
+ json_payload: str | None = None
70
+
71
+ def as_text(self) -> str:
72
+ return self.text or (self.json_payload or "")
73
+
74
+ def has_json(self) -> bool:
75
+ return self.json_payload is not None
76
+
77
+ def __str__(self) -> str: # pragma: no cover - convenience for logging
78
+ return self.as_text()
79
+
80
+
64
81
  @dataclass
65
82
  class SessionEventMarkovBlanketMessage:
66
83
  """Message crossing Markov blanket boundaries between systems in a session.
@@ -97,7 +114,7 @@ class SessionEventMarkovBlanketMessage:
97
114
  - 'causal_influence': Direction of causal flow
98
115
  """
99
116
 
100
- content: str
117
+ content: SessionMessageContent
101
118
  message_type: str
102
119
  time_record: TimeRecord
103
120
  metadata: dict[str, Any] = field(default_factory=dict)
@@ -232,7 +249,7 @@ class SessionTimeStep:
232
249
 
233
250
  step_id: str = ""
234
251
  step_index: int = 0
235
- timestamp: datetime = field(default_factory=datetime.utcnow)
252
+ timestamp: datetime = field(default_factory=lambda: datetime.now(UTC))
236
253
  turn_number: int | None = None
237
254
  events: list[BaseEvent] = field(default_factory=list)
238
255
  markov_blanket_messages: list[SessionEventMarkovBlanketMessage] = field(default_factory=list)
@@ -266,7 +283,7 @@ class SessionTrace:
266
283
  """
267
284
 
268
285
  session_id: str = ""
269
- created_at: datetime = field(default_factory=datetime.utcnow)
286
+ created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
270
287
  session_time_steps: list[SessionTimeStep] = field(default_factory=list)
271
288
  event_history: list[BaseEvent] = field(default_factory=list)
272
289
  markov_blanket_message_history: list[SessionEventMarkovBlanketMessage] = field(
@@ -1,5 +1,3 @@
1
- from __future__ import annotations
2
-
3
1
  """Async-aware decorators for tracing v3.
4
2
 
5
3
  This module provides decorators and context management utilities for the tracing
@@ -24,6 +22,8 @@ The decorators support both sync and async functions where appropriate,
24
22
  though async is preferred for consistency with the rest of the system.
25
23
  """
26
24
 
25
+ from __future__ import annotations
26
+
27
27
  import asyncio
28
28
  import contextvars
29
29
  import functools
@@ -37,12 +37,8 @@ from .utils import calculate_cost, detect_provider
37
37
  # Context variables for session and turn tracking
38
38
  # These variables automatically propagate across async call boundaries,
39
39
  # allowing deeply nested code to access tracing context without explicit passing
40
- _session_id_ctx: contextvars.ContextVar[str | None] = contextvars.ContextVar(
41
- "session_id", default=None
42
- )
43
- _turn_number_ctx: contextvars.ContextVar[int | None] = contextvars.ContextVar(
44
- "turn_number", default=None
45
- )
40
+ _session_id_ctx: contextvars.ContextVar[str | None] = contextvars.ContextVar("session_id")
41
+ _turn_number_ctx: contextvars.ContextVar[int | None] = contextvars.ContextVar("turn_number")
46
42
  _session_tracer_ctx: contextvars.ContextVar[Any | None] = contextvars.ContextVar(
47
43
  "session_tracer", default=None
48
44
  )
@@ -120,7 +116,9 @@ def with_session(require: bool = True):
120
116
  async def async_wrapper(*args, **kwargs):
121
117
  session_id = get_session_id()
122
118
  if require and session_id is None:
123
- raise RuntimeError(f"No active session for {fn.__name__}")
119
+ raise RuntimeError(
120
+ f"No active session for {getattr(fn, '__name__', 'unknown')}"
121
+ )
124
122
  return await fn(*args, **kwargs)
125
123
 
126
124
  return async_wrapper
@@ -130,7 +128,9 @@ def with_session(require: bool = True):
130
128
  def sync_wrapper(*args, **kwargs):
131
129
  session_id = get_session_id()
132
130
  if require and session_id is None:
133
- raise RuntimeError(f"No active session for {fn.__name__}")
131
+ raise RuntimeError(
132
+ f"No active session for {getattr(fn, '__name__', 'unknown')}"
133
+ )
134
134
  return fn(*args, **kwargs)
135
135
 
136
136
  return sync_wrapper
@@ -139,7 +139,7 @@ def with_session(require: bool = True):
139
139
 
140
140
 
141
141
  def trace_llm_call(
142
- model_name: str = None,
142
+ model_name: str | None = None,
143
143
  system_id: str = "llm",
144
144
  extract_tokens: bool = True,
145
145
  extract_cost: bool = True,
@@ -209,14 +209,16 @@ def trace_llm_call(
209
209
  input_tokens=input_tokens,
210
210
  output_tokens=output_tokens,
211
211
  total_tokens=total_tokens,
212
- cost_usd=calculate_cost(actual_model, input_tokens or 0, output_tokens or 0)
212
+ cost_usd=calculate_cost(
213
+ actual_model or "unknown", input_tokens or 0, output_tokens or 0
214
+ )
213
215
  if extract_cost
214
216
  else None,
215
217
  latency_ms=latency_ms,
216
218
  system_state_before=system_state_before,
217
219
  system_state_after=kwargs.get("state_after", {}),
218
220
  metadata={
219
- "function": fn.__name__,
221
+ "function": getattr(fn, "__name__", "unknown"),
220
222
  "step_id": kwargs.get("step_id"),
221
223
  },
222
224
  )
@@ -235,7 +237,7 @@ def trace_llm_call(
235
237
  provider=detect_provider(model_name),
236
238
  latency_ms=int((time.time() - start_time) * 1000),
237
239
  metadata={
238
- "function": fn.__name__,
240
+ "function": getattr(fn, "__name__", "unknown"),
239
241
  "error": str(e),
240
242
  "error_type": type(e).__name__,
241
243
  },
@@ -250,7 +252,7 @@ def trace_llm_call(
250
252
  return decorator
251
253
 
252
254
 
253
- def trace_method(event_type: str = "runtime", system_id: str = None):
255
+ def trace_method(event_type: str = "runtime", system_id: str | None = None):
254
256
  """Generic method tracing decorator.
255
257
 
256
258
  Traces any method call by recording it as a RuntimeEvent. Supports both
@@ -289,7 +291,7 @@ def trace_method(event_type: str = "runtime", system_id: str = None):
289
291
  time_record=TimeRecord(event_time=time.time()),
290
292
  actions=[], # Can be overridden in metadata
291
293
  metadata={
292
- "method": fn.__name__,
294
+ "method": getattr(fn, "__name__", "unknown"),
293
295
  "args": str(args)[:100], # Truncate for safety
294
296
  "step_id": kwargs.get("step_id"),
295
297
  },
@@ -1,5 +1,3 @@
1
- from __future__ import annotations
2
-
3
1
  """Hook system for extending tracing functionality.
4
2
 
5
3
  The hook system provides a flexible way to extend the tracing system without
@@ -34,6 +32,8 @@ Common Use Cases:
34
32
  - Custom filtering and sampling
35
33
  """
36
34
 
35
+ from __future__ import annotations
36
+
37
37
  import asyncio
38
38
  from collections.abc import Callable
39
39
  from dataclasses import dataclass
@@ -89,9 +89,9 @@ class HookManager:
89
89
  self,
90
90
  event: str,
91
91
  callback: Callable,
92
- name: str = None,
92
+ name: str | None = None,
93
93
  priority: int = 0,
94
- event_types: list[str] = None,
94
+ event_types: list[str] | None = None,
95
95
  ) -> Hook:
96
96
  """Register a new hook.
97
97
 
@@ -115,7 +115,7 @@ class HookManager:
115
115
  raise ValueError(f"Unknown hook event: {event}")
116
116
 
117
117
  hook = Hook(
118
- name=name or callback.__name__,
118
+ name=name or getattr(callback, "__name__", "unknown"),
119
119
  callback=callback,
120
120
  event_types=event_types,
121
121
  priority=priority,
@@ -5,10 +5,9 @@ format and compute aggregates from call records.
5
5
  """
6
6
 
7
7
  import uuid
8
- from datetime import datetime
8
+ from datetime import UTC, datetime
9
9
  from typing import Any
10
10
 
11
- from synth_ai.lm.vendors.base import BaseLMResponse
12
11
  from synth_ai.tracing_v3.lm_call_record_abstractions import (
13
12
  LLMCallRecord,
14
13
  LLMChunk,
@@ -18,6 +17,7 @@ from synth_ai.tracing_v3.lm_call_record_abstractions import (
18
17
  LLMUsage,
19
18
  ToolCallSpec,
20
19
  )
20
+ from synth_ai.v0.lm.vendors.base import BaseLMResponse
21
21
 
22
22
 
23
23
  def create_llm_call_record_from_response(
@@ -161,8 +161,8 @@ def create_llm_call_record_from_response(
161
161
  api_type=api_type,
162
162
  provider=provider,
163
163
  model_name=model_name,
164
- started_at=started_at or datetime.utcnow(),
165
- completed_at=completed_at or datetime.utcnow(),
164
+ started_at=started_at or datetime.now(UTC),
165
+ completed_at=completed_at or datetime.now(UTC),
166
166
  latency_ms=latency_ms,
167
167
  request_params=params,
168
168
  input_messages=input_messages,
@@ -322,8 +322,8 @@ def create_llm_call_record_from_streaming(
322
322
  api_type="responses", # Streaming typically from Responses API
323
323
  provider=provider,
324
324
  model_name=model_name,
325
- started_at=started_at or datetime.utcnow(),
326
- completed_at=completed_at or datetime.utcnow(),
325
+ started_at=started_at or datetime.now(UTC),
326
+ completed_at=completed_at or datetime.now(UTC),
327
327
  latency_ms=latency_ms,
328
328
  request_params=params,
329
329
  input_messages=input_messages,