synth-ai 0.2.4.dev6__py3-none-any.whl → 0.2.4.dev8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. synth_ai/__init__.py +18 -9
  2. synth_ai/cli/__init__.py +10 -5
  3. synth_ai/cli/balance.py +25 -32
  4. synth_ai/cli/calc.py +2 -3
  5. synth_ai/cli/demo.py +3 -5
  6. synth_ai/cli/legacy_root_backup.py +58 -32
  7. synth_ai/cli/man.py +22 -19
  8. synth_ai/cli/recent.py +9 -8
  9. synth_ai/cli/root.py +58 -13
  10. synth_ai/cli/status.py +13 -6
  11. synth_ai/cli/traces.py +45 -21
  12. synth_ai/cli/watch.py +40 -37
  13. synth_ai/config/base_url.py +47 -2
  14. synth_ai/core/experiment.py +1 -2
  15. synth_ai/environments/__init__.py +2 -6
  16. synth_ai/environments/environment/artifacts/base.py +3 -1
  17. synth_ai/environments/environment/db/sqlite.py +1 -1
  18. synth_ai/environments/environment/registry.py +19 -20
  19. synth_ai/environments/environment/resources/sqlite.py +2 -3
  20. synth_ai/environments/environment/rewards/core.py +3 -2
  21. synth_ai/environments/environment/tools/__init__.py +6 -4
  22. synth_ai/environments/examples/crafter_classic/__init__.py +1 -1
  23. synth_ai/environments/examples/crafter_classic/engine.py +13 -13
  24. synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +1 -0
  25. synth_ai/environments/examples/crafter_classic/engine_helpers/action_map.py +2 -1
  26. synth_ai/environments/examples/crafter_classic/engine_helpers/serialization.py +2 -1
  27. synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +3 -2
  28. synth_ai/environments/examples/crafter_classic/environment.py +16 -15
  29. synth_ai/environments/examples/crafter_classic/taskset.py +2 -2
  30. synth_ai/environments/examples/crafter_classic/trace_hooks_v3.py +2 -3
  31. synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +2 -1
  32. synth_ai/environments/examples/crafter_custom/crafter/__init__.py +2 -2
  33. synth_ai/environments/examples/crafter_custom/crafter/config.py +2 -2
  34. synth_ai/environments/examples/crafter_custom/crafter/env.py +1 -5
  35. synth_ai/environments/examples/crafter_custom/crafter/objects.py +1 -2
  36. synth_ai/environments/examples/crafter_custom/crafter/worldgen.py +1 -2
  37. synth_ai/environments/examples/crafter_custom/dataset_builder.py +5 -5
  38. synth_ai/environments/examples/crafter_custom/environment.py +13 -13
  39. synth_ai/environments/examples/crafter_custom/run_dataset.py +5 -5
  40. synth_ai/environments/examples/enron/art_helpers/email_search_tools.py +2 -2
  41. synth_ai/environments/examples/enron/art_helpers/local_email_db.py +5 -4
  42. synth_ai/environments/examples/enron/art_helpers/types_enron.py +2 -1
  43. synth_ai/environments/examples/enron/engine.py +18 -14
  44. synth_ai/environments/examples/enron/environment.py +12 -11
  45. synth_ai/environments/examples/enron/taskset.py +7 -7
  46. synth_ai/environments/examples/minigrid/__init__.py +6 -6
  47. synth_ai/environments/examples/minigrid/engine.py +6 -6
  48. synth_ai/environments/examples/minigrid/environment.py +6 -6
  49. synth_ai/environments/examples/minigrid/puzzle_loader.py +3 -2
  50. synth_ai/environments/examples/minigrid/taskset.py +13 -13
  51. synth_ai/environments/examples/nethack/achievements.py +1 -1
  52. synth_ai/environments/examples/nethack/engine.py +8 -7
  53. synth_ai/environments/examples/nethack/environment.py +10 -9
  54. synth_ai/environments/examples/nethack/helpers/__init__.py +8 -9
  55. synth_ai/environments/examples/nethack/helpers/action_mapping.py +1 -1
  56. synth_ai/environments/examples/nethack/helpers/nle_wrapper.py +2 -1
  57. synth_ai/environments/examples/nethack/helpers/observation_utils.py +1 -1
  58. synth_ai/environments/examples/nethack/helpers/recording_wrapper.py +3 -4
  59. synth_ai/environments/examples/nethack/helpers/trajectory_recorder.py +6 -5
  60. synth_ai/environments/examples/nethack/helpers/visualization/replay_viewer.py +5 -5
  61. synth_ai/environments/examples/nethack/helpers/visualization/visualizer.py +7 -6
  62. synth_ai/environments/examples/nethack/taskset.py +5 -5
  63. synth_ai/environments/examples/red/engine.py +9 -8
  64. synth_ai/environments/examples/red/engine_helpers/reward_components.py +2 -1
  65. synth_ai/environments/examples/red/engine_helpers/reward_library/__init__.py +7 -7
  66. synth_ai/environments/examples/red/engine_helpers/reward_library/adaptive_rewards.py +2 -1
  67. synth_ai/environments/examples/red/engine_helpers/reward_library/battle_rewards.py +2 -1
  68. synth_ai/environments/examples/red/engine_helpers/reward_library/composite_rewards.py +2 -1
  69. synth_ai/environments/examples/red/engine_helpers/reward_library/economy_rewards.py +2 -1
  70. synth_ai/environments/examples/red/engine_helpers/reward_library/efficiency_rewards.py +2 -1
  71. synth_ai/environments/examples/red/engine_helpers/reward_library/exploration_rewards.py +2 -1
  72. synth_ai/environments/examples/red/engine_helpers/reward_library/novelty_rewards.py +2 -1
  73. synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_rewards.py +2 -1
  74. synth_ai/environments/examples/red/engine_helpers/reward_library/pokemon_rewards.py +2 -1
  75. synth_ai/environments/examples/red/engine_helpers/reward_library/social_rewards.py +2 -1
  76. synth_ai/environments/examples/red/engine_helpers/reward_library/story_rewards.py +2 -1
  77. synth_ai/environments/examples/red/engine_helpers/screen_analysis.py +3 -2
  78. synth_ai/environments/examples/red/engine_helpers/state_extraction.py +2 -1
  79. synth_ai/environments/examples/red/environment.py +18 -15
  80. synth_ai/environments/examples/red/taskset.py +5 -3
  81. synth_ai/environments/examples/sokoban/engine.py +16 -13
  82. synth_ai/environments/examples/sokoban/engine_helpers/room_utils.py +3 -2
  83. synth_ai/environments/examples/sokoban/engine_helpers/vendored/__init__.py +2 -1
  84. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/__init__.py +1 -1
  85. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/boxoban_env.py +7 -5
  86. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/render_utils.py +1 -1
  87. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/room_utils.py +2 -1
  88. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env.py +5 -4
  89. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_fixed_targets.py +3 -2
  90. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_pull.py +2 -1
  91. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_two_player.py +5 -4
  92. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_variations.py +1 -1
  93. synth_ai/environments/examples/sokoban/environment.py +15 -14
  94. synth_ai/environments/examples/sokoban/generate_verified_puzzles.py +5 -3
  95. synth_ai/environments/examples/sokoban/puzzle_loader.py +3 -2
  96. synth_ai/environments/examples/sokoban/taskset.py +13 -10
  97. synth_ai/environments/examples/tictactoe/engine.py +6 -6
  98. synth_ai/environments/examples/tictactoe/environment.py +8 -7
  99. synth_ai/environments/examples/tictactoe/taskset.py +6 -5
  100. synth_ai/environments/examples/verilog/engine.py +4 -3
  101. synth_ai/environments/examples/verilog/environment.py +11 -10
  102. synth_ai/environments/examples/verilog/taskset.py +14 -12
  103. synth_ai/environments/examples/wordle/__init__.py +5 -5
  104. synth_ai/environments/examples/wordle/engine.py +32 -25
  105. synth_ai/environments/examples/wordle/environment.py +21 -16
  106. synth_ai/environments/examples/wordle/helpers/generate_instances_wordfreq.py +6 -6
  107. synth_ai/environments/examples/wordle/taskset.py +20 -12
  108. synth_ai/environments/reproducibility/core.py +1 -1
  109. synth_ai/environments/reproducibility/tree.py +21 -21
  110. synth_ai/environments/service/app.py +3 -2
  111. synth_ai/environments/service/core_routes.py +104 -110
  112. synth_ai/environments/service/external_registry.py +1 -2
  113. synth_ai/environments/service/registry.py +1 -1
  114. synth_ai/environments/stateful/core.py +1 -2
  115. synth_ai/environments/stateful/engine.py +1 -1
  116. synth_ai/environments/tasks/api.py +4 -4
  117. synth_ai/environments/tasks/core.py +14 -12
  118. synth_ai/environments/tasks/filters.py +6 -4
  119. synth_ai/environments/tasks/utils.py +13 -11
  120. synth_ai/evals/base.py +2 -3
  121. synth_ai/experimental/synth_oss.py +4 -4
  122. synth_ai/http.py +102 -0
  123. synth_ai/inference/__init__.py +7 -0
  124. synth_ai/inference/client.py +20 -0
  125. synth_ai/jobs/client.py +246 -0
  126. synth_ai/learning/__init__.py +24 -0
  127. synth_ai/learning/client.py +149 -0
  128. synth_ai/learning/config.py +43 -0
  129. synth_ai/learning/constants.py +29 -0
  130. synth_ai/learning/ft_client.py +59 -0
  131. synth_ai/learning/gateway.py +1 -3
  132. synth_ai/learning/health.py +43 -0
  133. synth_ai/learning/jobs.py +205 -0
  134. synth_ai/learning/prompts/banking77_injection_eval.py +15 -10
  135. synth_ai/learning/prompts/hello_world_in_context_injection_ex.py +26 -14
  136. synth_ai/learning/prompts/mipro.py +61 -52
  137. synth_ai/learning/prompts/random_search.py +42 -43
  138. synth_ai/learning/prompts/run_mipro_banking77.py +32 -20
  139. synth_ai/learning/prompts/run_random_search_banking77.py +71 -52
  140. synth_ai/learning/rl_client.py +256 -0
  141. synth_ai/learning/sse.py +58 -0
  142. synth_ai/learning/validators.py +48 -0
  143. synth_ai/lm/__init__.py +5 -5
  144. synth_ai/lm/caching/ephemeral.py +9 -9
  145. synth_ai/lm/caching/handler.py +20 -20
  146. synth_ai/lm/caching/persistent.py +10 -10
  147. synth_ai/lm/config.py +3 -3
  148. synth_ai/lm/constants.py +7 -7
  149. synth_ai/lm/core/all.py +17 -3
  150. synth_ai/lm/core/exceptions.py +0 -2
  151. synth_ai/lm/core/main.py +26 -41
  152. synth_ai/lm/core/main_v3.py +33 -10
  153. synth_ai/lm/core/synth_models.py +48 -0
  154. synth_ai/lm/core/vendor_clients.py +26 -22
  155. synth_ai/lm/injection.py +7 -8
  156. synth_ai/lm/overrides.py +21 -19
  157. synth_ai/lm/provider_support/__init__.py +1 -1
  158. synth_ai/lm/provider_support/anthropic.py +15 -15
  159. synth_ai/lm/provider_support/openai.py +23 -21
  160. synth_ai/lm/structured_outputs/handler.py +34 -32
  161. synth_ai/lm/structured_outputs/inject.py +24 -27
  162. synth_ai/lm/structured_outputs/rehabilitate.py +19 -15
  163. synth_ai/lm/tools/base.py +17 -16
  164. synth_ai/lm/unified_interface.py +17 -18
  165. synth_ai/lm/vendors/base.py +20 -18
  166. synth_ai/lm/vendors/core/anthropic_api.py +36 -27
  167. synth_ai/lm/vendors/core/gemini_api.py +31 -36
  168. synth_ai/lm/vendors/core/mistral_api.py +19 -19
  169. synth_ai/lm/vendors/core/openai_api.py +42 -13
  170. synth_ai/lm/vendors/openai_standard.py +158 -101
  171. synth_ai/lm/vendors/openai_standard_responses.py +74 -61
  172. synth_ai/lm/vendors/retries.py +9 -1
  173. synth_ai/lm/vendors/supported/custom_endpoint.py +38 -28
  174. synth_ai/lm/vendors/supported/deepseek.py +10 -10
  175. synth_ai/lm/vendors/supported/grok.py +8 -8
  176. synth_ai/lm/vendors/supported/ollama.py +2 -1
  177. synth_ai/lm/vendors/supported/openrouter.py +11 -9
  178. synth_ai/lm/vendors/synth_client.py +425 -75
  179. synth_ai/lm/warmup.py +8 -7
  180. synth_ai/rl/__init__.py +30 -0
  181. synth_ai/rl/contracts.py +32 -0
  182. synth_ai/rl/env_keys.py +137 -0
  183. synth_ai/rl/secrets.py +19 -0
  184. synth_ai/scripts/verify_rewards.py +100 -0
  185. synth_ai/task/__init__.py +10 -0
  186. synth_ai/task/contracts.py +120 -0
  187. synth_ai/task/health.py +28 -0
  188. synth_ai/task/validators.py +12 -0
  189. synth_ai/tracing/__init__.py +22 -10
  190. synth_ai/tracing_v1/__init__.py +22 -20
  191. synth_ai/tracing_v3/__init__.py +7 -7
  192. synth_ai/tracing_v3/abstractions.py +56 -52
  193. synth_ai/tracing_v3/config.py +4 -2
  194. synth_ai/tracing_v3/db_config.py +6 -8
  195. synth_ai/tracing_v3/decorators.py +29 -30
  196. synth_ai/tracing_v3/examples/basic_usage.py +12 -12
  197. synth_ai/tracing_v3/hooks.py +24 -22
  198. synth_ai/tracing_v3/llm_call_record_helpers.py +85 -98
  199. synth_ai/tracing_v3/lm_call_record_abstractions.py +2 -4
  200. synth_ai/tracing_v3/migration_helper.py +3 -5
  201. synth_ai/tracing_v3/replica_sync.py +30 -32
  202. synth_ai/tracing_v3/session_tracer.py +158 -31
  203. synth_ai/tracing_v3/storage/__init__.py +1 -1
  204. synth_ai/tracing_v3/storage/base.py +8 -7
  205. synth_ai/tracing_v3/storage/config.py +4 -4
  206. synth_ai/tracing_v3/storage/factory.py +4 -4
  207. synth_ai/tracing_v3/storage/utils.py +9 -9
  208. synth_ai/tracing_v3/turso/__init__.py +3 -3
  209. synth_ai/tracing_v3/turso/daemon.py +9 -9
  210. synth_ai/tracing_v3/turso/manager.py +278 -48
  211. synth_ai/tracing_v3/turso/models.py +77 -19
  212. synth_ai/tracing_v3/utils.py +5 -5
  213. synth_ai/v0/tracing/abstractions.py +28 -28
  214. synth_ai/v0/tracing/base_client.py +9 -9
  215. synth_ai/v0/tracing/client_manager.py +7 -7
  216. synth_ai/v0/tracing/config.py +7 -7
  217. synth_ai/v0/tracing/context.py +6 -6
  218. synth_ai/v0/tracing/decorators.py +6 -5
  219. synth_ai/v0/tracing/events/manage.py +1 -1
  220. synth_ai/v0/tracing/events/store.py +5 -4
  221. synth_ai/v0/tracing/immediate_client.py +4 -5
  222. synth_ai/v0/tracing/local.py +3 -3
  223. synth_ai/v0/tracing/log_client_base.py +4 -5
  224. synth_ai/v0/tracing/retry_queue.py +5 -6
  225. synth_ai/v0/tracing/trackers.py +25 -25
  226. synth_ai/v0/tracing/upload.py +6 -0
  227. synth_ai/v0/tracing_v1/__init__.py +1 -1
  228. synth_ai/v0/tracing_v1/abstractions.py +28 -28
  229. synth_ai/v0/tracing_v1/base_client.py +9 -9
  230. synth_ai/v0/tracing_v1/client_manager.py +7 -7
  231. synth_ai/v0/tracing_v1/config.py +7 -7
  232. synth_ai/v0/tracing_v1/context.py +6 -6
  233. synth_ai/v0/tracing_v1/decorators.py +7 -6
  234. synth_ai/v0/tracing_v1/events/manage.py +1 -1
  235. synth_ai/v0/tracing_v1/events/store.py +5 -4
  236. synth_ai/v0/tracing_v1/immediate_client.py +4 -5
  237. synth_ai/v0/tracing_v1/local.py +3 -3
  238. synth_ai/v0/tracing_v1/log_client_base.py +4 -5
  239. synth_ai/v0/tracing_v1/retry_queue.py +5 -6
  240. synth_ai/v0/tracing_v1/trackers.py +25 -25
  241. synth_ai/v0/tracing_v1/upload.py +25 -24
  242. synth_ai/zyk/__init__.py +1 -0
  243. synth_ai-0.2.4.dev8.dist-info/METADATA +635 -0
  244. synth_ai-0.2.4.dev8.dist-info/RECORD +317 -0
  245. synth_ai/tui/__init__.py +0 -1
  246. synth_ai/tui/__main__.py +0 -13
  247. synth_ai/tui/cli/__init__.py +0 -1
  248. synth_ai/tui/cli/query_experiments.py +0 -165
  249. synth_ai/tui/cli/query_experiments_v3.py +0 -165
  250. synth_ai/tui/dashboard.py +0 -329
  251. synth_ai-0.2.4.dev6.dist-info/METADATA +0 -203
  252. synth_ai-0.2.4.dev6.dist-info/RECORD +0 -299
  253. {synth_ai-0.2.4.dev6.dist-info → synth_ai-0.2.4.dev8.dist-info}/WHEEL +0 -0
  254. {synth_ai-0.2.4.dev6.dist-info → synth_ai-0.2.4.dev8.dist-info}/entry_points.txt +0 -0
  255. {synth_ai-0.2.4.dev6.dist-info → synth_ai-0.2.4.dev8.dist-info}/licenses/LICENSE +0 -0
  256. {synth_ai-0.2.4.dev6.dist-info → synth_ai-0.2.4.dev8.dist-info}/top_level.txt +0 -0
@@ -1,15 +1,21 @@
1
1
  """Main SessionTracer class for tracing v3."""
2
2
 
3
3
  import asyncio
4
- from datetime import datetime
5
- from typing import Dict, List, Optional, Any, Union
6
4
  from contextlib import asynccontextmanager
7
-
8
- from .abstractions import SessionTrace, SessionTimeStep, BaseEvent, SessionEventMarkovBlanketMessage, TimeRecord
9
- from .decorators import set_session_id, set_turn_number, set_session_tracer, SessionContext
10
- from .turso.manager import AsyncSQLTraceManager
5
+ from datetime import datetime
6
+ from typing import Any
7
+
8
+ from .abstractions import (
9
+ BaseEvent,
10
+ SessionEventMarkovBlanketMessage,
11
+ SessionTimeStep,
12
+ SessionTrace,
13
+ TimeRecord,
14
+ )
11
15
  from .config import CONFIG
12
- from .hooks import HookManager, GLOBAL_HOOKS
16
+ from .decorators import set_session_id, set_session_tracer, set_turn_number
17
+ from .hooks import GLOBAL_HOOKS, HookManager
18
+ from .turso.manager import AsyncSQLTraceManager
13
19
  from .utils import generate_session_id
14
20
 
15
21
 
@@ -18,8 +24,8 @@ class SessionTracer:
18
24
 
19
25
  def __init__(
20
26
  self,
21
- hooks: Optional[HookManager] = None,
22
- db_url: Optional[str] = None,
27
+ hooks: HookManager | None = None,
28
+ db_url: str | None = None,
23
29
  auto_save: bool = True,
24
30
  ):
25
31
  """Initialize session tracer.
@@ -30,20 +36,20 @@ class SessionTracer:
30
36
  auto_save: Whether to automatically save sessions on end
31
37
  """
32
38
  self.hooks = hooks or GLOBAL_HOOKS
33
- self._current_trace: Optional[SessionTrace] = None
39
+ self._current_trace: SessionTrace | None = None
34
40
  self._lock = asyncio.Lock()
35
41
  self.db_url = db_url or CONFIG.db_url
36
- self.db: Optional[AsyncSQLTraceManager] = None
42
+ self.db: AsyncSQLTraceManager | None = None
37
43
  self.auto_save = auto_save
38
- self._current_step: Optional[SessionTimeStep] = None
44
+ self._current_step: SessionTimeStep | None = None
39
45
 
40
46
  @property
41
- def current_session(self) -> Optional[SessionTrace]:
47
+ def current_session(self) -> SessionTrace | None:
42
48
  """Get the current session trace."""
43
49
  return self._current_trace
44
50
 
45
51
  @property
46
- def current_step(self) -> Optional[SessionTimeStep]:
52
+ def current_step(self) -> SessionTimeStep | None:
47
53
  """Get the current timestep."""
48
54
  return self._current_step
49
55
 
@@ -54,14 +60,14 @@ class SessionTracer:
54
60
  await self.db.initialize()
55
61
 
56
62
  async def start_session(
57
- self, session_id: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None
63
+ self, session_id: str | None = None, metadata: dict[str, Any] | None = None
58
64
  ) -> str:
59
65
  """Start a new session.
60
-
66
+
61
67
  Creates a new tracing session and sets up the necessary context variables.
62
68
  This method is thread-safe and will raise an error if a session is already
63
69
  active to prevent accidental session mixing.
64
-
70
+
65
71
  The session ID is propagated through asyncio context variables, allowing
66
72
  nested async functions to access the tracer without explicit passing.
67
73
 
@@ -76,7 +82,7 @@ class SessionTracer:
76
82
 
77
83
  Returns:
78
84
  The session ID (either provided or generated)
79
-
85
+
80
86
  Raises:
81
87
  RuntimeError: If a session is already active
82
88
  """
@@ -101,6 +107,10 @@ class SessionTracer:
101
107
  if self.auto_save and self.db is None:
102
108
  await self.initialize()
103
109
 
110
+ # Ensure session row exists for incremental writes
111
+ if self.db:
112
+ await self.db.ensure_session(session_id, created_at=self._current_trace.created_at, metadata=metadata or {})
113
+
104
114
  # Trigger hooks
105
115
  await self.hooks.trigger(
106
116
  "session_start", session_id=session_id, metadata=metadata or {}
@@ -111,8 +121,8 @@ class SessionTracer:
111
121
  async def start_timestep(
112
122
  self,
113
123
  step_id: str,
114
- turn_number: Optional[int] = None,
115
- metadata: Optional[Dict[str, Any]] = None,
124
+ turn_number: int | None = None,
125
+ metadata: dict[str, Any] | None = None,
116
126
  ) -> SessionTimeStep:
117
127
  """Start a new timestep.
118
128
 
@@ -146,9 +156,20 @@ class SessionTracer:
146
156
  "timestep_start", step=step, session_id=self._current_trace.session_id
147
157
  )
148
158
 
159
+ # Ensure timestep row exists in DB for incremental linkage
160
+ if self.db:
161
+ await self.db.ensure_timestep(
162
+ self._current_trace.session_id,
163
+ step_id=step.step_id,
164
+ step_index=step.step_index,
165
+ turn_number=turn_number,
166
+ started_at=step.timestamp,
167
+ metadata=metadata or {},
168
+ )
169
+
149
170
  return step
150
171
 
151
- async def end_timestep(self, step_id: Optional[str] = None):
172
+ async def end_timestep(self, step_id: str | None = None):
152
173
  """End the current or specified timestep."""
153
174
  if self._current_trace is None:
154
175
  raise RuntimeError("No active session")
@@ -174,7 +195,7 @@ class SessionTracer:
174
195
  if step == self._current_step:
175
196
  self._current_step = None
176
197
 
177
- async def record_event(self, event: BaseEvent):
198
+ async def record_event(self, event: BaseEvent) -> int | None:
178
199
  """Record an event.
179
200
 
180
201
  Args:
@@ -195,14 +216,54 @@ class SessionTracer:
195
216
  if self._current_step:
196
217
  self._current_step.events.append(event)
197
218
 
219
+ # Persist incrementally if DB is available; return DB event id
220
+ if self.db:
221
+ timestep_db_id = None
222
+ if self._current_step:
223
+ # ensure timestep exists and get id
224
+ timestep_db_id = await self.db.ensure_timestep(
225
+ self._current_trace.session_id,
226
+ step_id=self._current_step.step_id,
227
+ step_index=self._current_step.step_index,
228
+ turn_number=self._current_step.turn_number,
229
+ started_at=self._current_step.timestamp,
230
+ completed_at=self._current_step.completed_at,
231
+ metadata=self._current_step.step_metadata,
232
+ )
233
+ event_id = await self.db.insert_event_row(
234
+ self._current_trace.session_id,
235
+ timestep_db_id=timestep_db_id,
236
+ event=event,
237
+ )
238
+ # Auto-insert an event reward if EnvironmentEvent carries reward
239
+ try:
240
+ from .abstractions import EnvironmentEvent # local import to avoid cycles
241
+
242
+ if isinstance(event, EnvironmentEvent) and event.reward is not None:
243
+ await self.record_event_reward(
244
+ event_id=event_id,
245
+ message_id=None,
246
+ turn_number=self._current_step.turn_number if self._current_step else None,
247
+ reward_value=float(event.reward),
248
+ reward_type="sparse",
249
+ key=None,
250
+ annotation=getattr(event, "event_metadata", None),
251
+ source="environment",
252
+ )
253
+ except Exception:
254
+ # Do not fail tracing if reward recording fails
255
+ pass
256
+ return event_id
257
+ return None
258
+
198
259
  async def record_message(
199
260
  self,
200
261
  content: str,
201
262
  message_type: str,
202
- event_time: Optional[float] = None,
203
- message_time: Optional[int] = None,
204
- metadata: Optional[Dict[str, Any]] = None,
205
- ):
263
+ event_time: float | None = None,
264
+ message_time: int | None = None,
265
+ metadata: dict[str, Any] | None = None,
266
+ ) -> int | None:
206
267
  """Record a message.
207
268
 
208
269
  Args:
@@ -236,6 +297,31 @@ class SessionTracer:
236
297
  if self._current_step:
237
298
  self._current_step.markov_blanket_messages.append(msg)
238
299
 
300
+ # Persist incrementally and return DB message id
301
+ if self.db:
302
+ timestep_db_id = None
303
+ if self._current_step:
304
+ timestep_db_id = await self.db.ensure_timestep(
305
+ self._current_trace.session_id,
306
+ step_id=self._current_step.step_id,
307
+ step_index=self._current_step.step_index,
308
+ turn_number=self._current_step.turn_number,
309
+ started_at=self._current_step.timestamp,
310
+ completed_at=self._current_step.completed_at,
311
+ metadata=self._current_step.step_metadata,
312
+ )
313
+ message_id = await self.db.insert_message_row(
314
+ self._current_trace.session_id,
315
+ timestep_db_id=timestep_db_id,
316
+ message_type=message_type,
317
+ content=content,
318
+ event_time=msg.time_record.event_time,
319
+ message_time=msg.time_record.message_time,
320
+ metadata=msg.metadata,
321
+ )
322
+ return message_id
323
+ return None
324
+
239
325
  async def end_session(self, save: bool = None) -> SessionTrace:
240
326
  """End the current session.
241
327
 
@@ -281,8 +367,8 @@ class SessionTracer:
281
367
  @asynccontextmanager
282
368
  async def session(
283
369
  self,
284
- session_id: Optional[str] = None,
285
- metadata: Optional[Dict[str, Any]] = None,
370
+ session_id: str | None = None,
371
+ metadata: dict[str, Any] | None = None,
286
372
  save: bool = None,
287
373
  ):
288
374
  """Context manager for a session.
@@ -302,8 +388,8 @@ class SessionTracer:
302
388
  async def timestep(
303
389
  self,
304
390
  step_id: str,
305
- turn_number: Optional[int] = None,
306
- metadata: Optional[Dict[str, Any]] = None,
391
+ turn_number: int | None = None,
392
+ metadata: dict[str, Any] | None = None,
307
393
  ):
308
394
  """Context manager for a timestep.
309
395
 
@@ -318,7 +404,7 @@ class SessionTracer:
318
404
  finally:
319
405
  await self.end_timestep(step_id)
320
406
 
321
- async def get_session_history(self, limit: Optional[int] = None) -> List[Dict[str, Any]]:
407
+ async def get_session_history(self, limit: int | None = None) -> list[dict[str, Any]]:
322
408
  """Get recent session history from database."""
323
409
  if self.db is None:
324
410
  await self.initialize()
@@ -335,3 +421,44 @@ class SessionTracer:
335
421
  if self.db:
336
422
  await self.db.close()
337
423
  self.db = None
424
+
425
+ # -------------------------------
426
+ # Reward recording helpers
427
+ # -------------------------------
428
+
429
+ async def record_outcome_reward(self, *, total_reward: int, achievements_count: int, total_steps: int) -> int | None:
430
+ """Record an episode-level outcome reward for the current session."""
431
+ if self._current_trace is None:
432
+ raise RuntimeError("No active session")
433
+ if self.db is None:
434
+ await self.initialize()
435
+ if self.db:
436
+ return await self.db.insert_outcome_reward(
437
+ self._current_trace.session_id,
438
+ total_reward=total_reward,
439
+ achievements_count=achievements_count,
440
+ total_steps=total_steps,
441
+ )
442
+ return None
443
+
444
+ # StepMetrics removed in favor of event_rewards; use record_event_reward for per-turn shaped values
445
+
446
+ async def record_event_reward(self, *, event_id: int, message_id: int | None = None, turn_number: int | None = None, reward_value: float = 0.0, reward_type: str | None = None, key: str | None = None, annotation: dict[str, Any] | None = None, source: str | None = None) -> int | None:
447
+ """Record a first-class event-level reward with optional annotations."""
448
+ if self._current_trace is None:
449
+ raise RuntimeError("No active session")
450
+ if self.db is None:
451
+ await self.initialize()
452
+ if self.db:
453
+ return await self.db.insert_event_reward(
454
+ self._current_trace.session_id,
455
+ event_id=event_id,
456
+ message_id=message_id,
457
+ turn_number=turn_number,
458
+ reward_value=reward_value,
459
+ reward_type=reward_type,
460
+ key=key,
461
+ annotation=annotation,
462
+ source=source,
463
+ )
464
+ return None
@@ -1,8 +1,8 @@
1
1
  """Storage abstraction layer for tracing v3."""
2
2
 
3
3
  from .base import TraceStorage
4
- from .factory import create_storage
5
4
  from .config import StorageConfig
5
+ from .factory import create_storage
6
6
  from .types import EventType, MessageType, Provider
7
7
 
8
8
  __all__ = [
@@ -2,7 +2,8 @@
2
2
 
3
3
  from abc import ABC, abstractmethod
4
4
  from datetime import datetime
5
- from typing import Any, Dict, List, Optional
5
+ from typing import Any
6
+
6
7
  import pandas as pd
7
8
 
8
9
  from ..abstractions import SessionTrace
@@ -29,7 +30,7 @@ class TraceStorage(ABC):
29
30
  pass
30
31
 
31
32
  @abstractmethod
32
- async def get_session_trace(self, session_id: str) -> Optional[Dict[str, Any]]:
33
+ async def get_session_trace(self, session_id: str) -> dict[str, Any] | None:
33
34
  """Retrieve a session trace by ID.
34
35
 
35
36
  Args:
@@ -41,7 +42,7 @@ class TraceStorage(ABC):
41
42
  pass
42
43
 
43
44
  @abstractmethod
44
- async def query_traces(self, query: str, params: Dict[str, Any] = None) -> pd.DataFrame:
45
+ async def query_traces(self, query: str, params: dict[str, Any] = None) -> pd.DataFrame:
45
46
  """Execute a query and return results as DataFrame.
46
47
 
47
48
  Args:
@@ -92,7 +93,7 @@ class TraceStorage(ABC):
92
93
  experiment_id: str,
93
94
  name: str,
94
95
  description: str = None,
95
- configuration: Dict[str, Any] = None,
96
+ configuration: dict[str, Any] = None,
96
97
  ) -> str:
97
98
  """Create a new experiment."""
98
99
  raise NotImplementedError("Experiment management not supported by this backend")
@@ -103,14 +104,14 @@ class TraceStorage(ABC):
103
104
 
104
105
  async def get_sessions_by_experiment(
105
106
  self, experiment_id: str, limit: int = None
106
- ) -> List[Dict[str, Any]]:
107
+ ) -> list[dict[str, Any]]:
107
108
  """Get all sessions for an experiment."""
108
109
  raise NotImplementedError("Experiment management not supported by this backend")
109
110
 
110
111
  # Batch operations
111
112
  async def batch_insert_sessions(
112
- self, traces: List[SessionTrace], batch_size: int = 1000
113
- ) -> List[str]:
113
+ self, traces: list[SessionTrace], batch_size: int = 1000
114
+ ) -> list[str]:
114
115
  """Batch insert multiple session traces.
115
116
 
116
117
  Default implementation calls insert_session_trace for each trace.
@@ -1,9 +1,9 @@
1
1
  """Storage configuration for tracing v3."""
2
2
 
3
- from dataclasses import dataclass
4
- from typing import Optional, Dict, Any
5
3
  import os
4
+ from dataclasses import dataclass
6
5
  from enum import Enum
6
+ from typing import Any
7
7
 
8
8
 
9
9
  class StorageBackend(str, Enum):
@@ -19,7 +19,7 @@ class StorageConfig:
19
19
  """Configuration for storage backend."""
20
20
 
21
21
  backend: StorageBackend = StorageBackend.TURSO
22
- connection_string: Optional[str] = None
22
+ connection_string: str | None = None
23
23
 
24
24
  # Turso-specific settings
25
25
  turso_url: str = os.getenv("TURSO_DATABASE_URL", "sqlite+libsql://http://127.0.0.1:8080")
@@ -48,7 +48,7 @@ class StorageConfig:
48
48
  else:
49
49
  raise ValueError(f"Unknown backend: {self.backend}")
50
50
 
51
- def get_backend_config(self) -> Dict[str, Any]:
51
+ def get_backend_config(self) -> dict[str, Any]:
52
52
  """Get backend-specific configuration."""
53
53
  if self.backend == StorageBackend.TURSO:
54
54
  config = {}
@@ -1,12 +1,12 @@
1
1
  """Factory for creating storage instances."""
2
2
 
3
- from typing import Optional
4
- from .base import TraceStorage
5
- from .config import StorageConfig, StorageBackend
3
+
6
4
  from ..turso.manager import AsyncSQLTraceManager
5
+ from .base import TraceStorage
6
+ from .config import StorageBackend, StorageConfig
7
7
 
8
8
 
9
- def create_storage(config: Optional[StorageConfig] = None) -> TraceStorage:
9
+ def create_storage(config: StorageConfig | None = None) -> TraceStorage:
10
10
  """Create a storage instance based on configuration.
11
11
 
12
12
  Args:
@@ -1,10 +1,10 @@
1
1
  """Utility functions for storage layer."""
2
2
 
3
3
  import asyncio
4
- from typing import Any, Dict, List, Optional, TypeVar, Callable
5
4
  import functools
6
5
  import time
7
-
6
+ from collections.abc import Callable
7
+ from typing import Any, TypeVar
8
8
 
9
9
  T = TypeVar("T")
10
10
 
@@ -43,8 +43,8 @@ def retry_async(max_attempts: int = 3, delay: float = 1.0, backoff: float = 2.0)
43
43
 
44
44
 
45
45
  async def batch_process(
46
- items: List[Any], processor: Callable, batch_size: int = 100, max_concurrent: int = 5
47
- ) -> List[Any]:
46
+ items: list[Any], processor: Callable, batch_size: int = 100, max_concurrent: int = 5
47
+ ) -> list[Any]:
48
48
  """Process items in batches with concurrency control.
49
49
 
50
50
  Args:
@@ -88,7 +88,7 @@ def sanitize_json(data: Any) -> Any:
88
88
  return {k: sanitize_json(v) for k, v in data.items()}
89
89
  elif isinstance(data, list):
90
90
  return [sanitize_json(item) for item in data]
91
- elif isinstance(data, (str, int, float, bool, type(None))):
91
+ elif isinstance(data, str | int | float | bool | type(None)):
92
92
  return data
93
93
  else:
94
94
  return str(data)
@@ -101,7 +101,7 @@ def estimate_size(obj: Any) -> int:
101
101
  """
102
102
  if isinstance(obj, str):
103
103
  return len(obj.encode("utf-8"))
104
- elif isinstance(obj, (int, float)):
104
+ elif isinstance(obj, int | float):
105
105
  return 8
106
106
  elif isinstance(obj, bool):
107
107
  return 1
@@ -120,10 +120,10 @@ class StorageMetrics:
120
120
  """Track storage operation metrics."""
121
121
 
122
122
  def __init__(self):
123
- self.operations: Dict[str, Dict[str, Any]] = {}
123
+ self.operations: dict[str, dict[str, Any]] = {}
124
124
 
125
125
  def record_operation(
126
- self, operation: str, duration: float, success: bool, size: Optional[int] = None
126
+ self, operation: str, duration: float, success: bool, size: int | None = None
127
127
  ):
128
128
  """Record a storage operation."""
129
129
  if operation not in self.operations:
@@ -142,7 +142,7 @@ class StorageMetrics:
142
142
  if size:
143
143
  stats["total_size"] += size
144
144
 
145
- def get_stats(self, operation: Optional[str] = None) -> Dict[str, Any]:
145
+ def get_stats(self, operation: str | None = None) -> dict[str, Any]:
146
146
  """Get statistics for operations."""
147
147
  if operation:
148
148
  stats = self.operations.get(operation, {})
@@ -3,11 +3,11 @@
3
3
  from .manager import AsyncSQLTraceManager
4
4
  from .models import (
5
5
  Base,
6
- SessionTrace,
7
- SessionTimestep,
8
6
  Event,
9
- Message,
10
7
  Experiment,
8
+ Message,
9
+ SessionTimestep,
10
+ SessionTrace,
11
11
  System,
12
12
  SystemVersion,
13
13
  )
@@ -1,12 +1,12 @@
1
1
  """sqld daemon management utilities."""
2
2
 
3
- import subprocess
4
3
  import pathlib
5
4
  import shutil
6
- import sys
5
+ import subprocess
7
6
  import time
7
+
8
8
  import requests
9
- from typing import Optional
9
+
10
10
  from ..config import CONFIG
11
11
 
12
12
 
@@ -15,23 +15,23 @@ class SqldDaemon:
15
15
 
16
16
  def __init__(self, db_path: str = None, http_port: int = None, binary_path: str = None):
17
17
  """Initialize sqld daemon manager.
18
-
18
+
19
19
  Args:
20
20
  db_path: Path to database file (uses config default if not provided)
21
- http_port: HTTP port for daemon (uses config default if not provided)
21
+ http_port: HTTP port for daemon (uses config default if not provided)
22
22
  binary_path: Path to sqld binary (auto-detected if not provided)
23
23
  """
24
24
  self.db_path = db_path or CONFIG.sqld_db_path
25
25
  self.http_port = http_port or CONFIG.sqld_http_port
26
26
  self.binary_path = binary_path or self._find_binary()
27
- self.process: Optional[subprocess.Popen] = None
27
+ self.process: subprocess.Popen | None = None
28
28
 
29
29
  def _find_binary(self) -> str:
30
30
  """Find sqld binary in PATH."""
31
31
  binary = shutil.which(CONFIG.sqld_binary) or shutil.which("libsql-server")
32
32
  if not binary:
33
33
  raise RuntimeError(
34
- f"sqld binary not found in PATH. Install with: brew install turso-tech/tools/sqld"
34
+ "sqld binary not found in PATH. Install with: brew install turso-tech/tools/sqld"
35
35
  )
36
36
  return binary
37
37
 
@@ -117,7 +117,7 @@ class SqldDaemon:
117
117
 
118
118
 
119
119
  # Convenience functions
120
- _daemon: Optional[SqldDaemon] = None
120
+ _daemon: SqldDaemon | None = None
121
121
 
122
122
 
123
123
  def start_sqld(db_path: str = None, port: int = None) -> SqldDaemon:
@@ -139,6 +139,6 @@ def stop_sqld():
139
139
  _daemon = None
140
140
 
141
141
 
142
- def get_daemon() -> Optional[SqldDaemon]:
142
+ def get_daemon() -> SqldDaemon | None:
143
143
  """Get the global daemon instance."""
144
144
  return _daemon