synth-ai 0.1.9__py3-none-any.whl → 0.2.1.dev0__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 (266) hide show
  1. synth_ai/__init__.py +28 -2
  2. synth_ai/core/system.py +4 -0
  3. synth_ai/environments/__init__.py +35 -0
  4. synth_ai/environments/environment/__init__.py +1 -0
  5. synth_ai/environments/environment/artifacts/__init__.py +1 -0
  6. synth_ai/environments/environment/artifacts/base.py +50 -0
  7. synth_ai/environments/environment/core.py +22 -0
  8. synth_ai/environments/environment/db/__init__.py +1 -0
  9. synth_ai/environments/environment/db/sqlite.py +45 -0
  10. synth_ai/environments/environment/registry.py +24 -0
  11. synth_ai/environments/environment/resources/sqlite.py +46 -0
  12. synth_ai/environments/environment/results.py +1 -0
  13. synth_ai/environments/environment/rewards/__init__.py +1 -0
  14. synth_ai/environments/environment/rewards/core.py +28 -0
  15. synth_ai/environments/environment/shared_engine.py +26 -0
  16. synth_ai/environments/environment/tools/__init__.py +34 -0
  17. synth_ai/environments/examples/__init__.py +1 -0
  18. synth_ai/environments/examples/crafter_classic/__init__.py +8 -0
  19. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_comprehensive_evaluation.py +58 -0
  20. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_evaluation_browser.py +152 -0
  21. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_evaluation_framework.py +1194 -0
  22. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_quick_evaluation.py +51 -0
  23. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_react_agent.py +872 -0
  24. synth_ai/environments/examples/crafter_classic/agent_demos/crafter_trace_evaluation.py +1412 -0
  25. synth_ai/environments/examples/crafter_classic/agent_demos/test_crafter_react_agent.py +1110 -0
  26. synth_ai/environments/examples/crafter_classic/config_logging.py +111 -0
  27. synth_ai/environments/examples/crafter_classic/engine.py +502 -0
  28. synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +63 -0
  29. synth_ai/environments/examples/crafter_classic/engine_helpers/action_map.py +5 -0
  30. synth_ai/environments/examples/crafter_classic/engine_helpers/serialization.py +74 -0
  31. synth_ai/environments/examples/crafter_classic/environment.py +255 -0
  32. synth_ai/environments/examples/crafter_classic/taskset.py +228 -0
  33. synth_ai/environments/examples/enron/agent_demos/test_synth_react.py +535 -0
  34. synth_ai/environments/examples/enron/art_helpers/email_search_tools.py +156 -0
  35. synth_ai/environments/examples/enron/art_helpers/local_email_db.py +280 -0
  36. synth_ai/environments/examples/enron/art_helpers/types_enron.py +24 -0
  37. synth_ai/environments/examples/enron/engine.py +291 -0
  38. synth_ai/environments/examples/enron/environment.py +165 -0
  39. synth_ai/environments/examples/enron/taskset.py +112 -0
  40. synth_ai/environments/examples/enron/units/keyword_stats.py +111 -0
  41. synth_ai/environments/examples/enron/units/test_email_index.py +8 -0
  42. synth_ai/environments/examples/minigrid/__init__.py +48 -0
  43. synth_ai/environments/examples/minigrid/agent_demos/minigrid_evaluation_framework.py +1188 -0
  44. synth_ai/environments/examples/minigrid/agent_demos/minigrid_quick_evaluation.py +47 -0
  45. synth_ai/environments/examples/minigrid/agent_demos/minigrid_react_agent.py +562 -0
  46. synth_ai/environments/examples/minigrid/agent_demos/minigrid_trace_evaluation.py +220 -0
  47. synth_ai/environments/examples/minigrid/agent_demos/test_minigrid_react_agent.py +393 -0
  48. synth_ai/environments/examples/minigrid/engine.py +589 -0
  49. synth_ai/environments/examples/minigrid/environment.py +274 -0
  50. synth_ai/environments/examples/minigrid/environment_mapping.py +242 -0
  51. synth_ai/environments/examples/minigrid/puzzle_loader.py +416 -0
  52. synth_ai/environments/examples/minigrid/taskset.py +583 -0
  53. synth_ai/environments/examples/minigrid/units/test_action_behavior.py +226 -0
  54. synth_ai/environments/examples/minigrid/units/test_debug_messages.py +83 -0
  55. synth_ai/environments/examples/minigrid/units/test_exploration.py +120 -0
  56. synth_ai/environments/examples/minigrid/units/test_minigrid_engine.py +214 -0
  57. synth_ai/environments/examples/minigrid/units/test_minigrid_environment.py +238 -0
  58. synth_ai/environments/examples/minigrid/units/test_minigrid_environment_mapping.py +301 -0
  59. synth_ai/environments/examples/minigrid/units/test_minigrid_taskset.py +210 -0
  60. synth_ai/environments/examples/nethack/__init__.py +7 -0
  61. synth_ai/environments/examples/nethack/achievements.py +337 -0
  62. synth_ai/environments/examples/nethack/agent_demos/nethack_evaluation_framework.py +981 -0
  63. synth_ai/environments/examples/nethack/agent_demos/nethack_quick_evaluation.py +74 -0
  64. synth_ai/environments/examples/nethack/agent_demos/nethack_react_agent.py +832 -0
  65. synth_ai/environments/examples/nethack/agent_demos/test_nethack_react_agent.py +1112 -0
  66. synth_ai/environments/examples/nethack/engine.py +738 -0
  67. synth_ai/environments/examples/nethack/environment.py +255 -0
  68. synth_ai/environments/examples/nethack/helpers/__init__.py +42 -0
  69. synth_ai/environments/examples/nethack/helpers/action_mapping.py +301 -0
  70. synth_ai/environments/examples/nethack/helpers/nle_wrapper.py +401 -0
  71. synth_ai/environments/examples/nethack/helpers/observation_utils.py +433 -0
  72. synth_ai/environments/examples/nethack/helpers/recording_wrapper.py +201 -0
  73. synth_ai/environments/examples/nethack/helpers/trajectory_recorder.py +268 -0
  74. synth_ai/environments/examples/nethack/helpers/visualization/replay_viewer.py +308 -0
  75. synth_ai/environments/examples/nethack/helpers/visualization/visualizer.py +430 -0
  76. synth_ai/environments/examples/nethack/taskset.py +323 -0
  77. synth_ai/environments/examples/nethack/units/test_nethack_engine.py +277 -0
  78. synth_ai/environments/examples/nethack/units/test_nethack_environment.py +281 -0
  79. synth_ai/environments/examples/nethack/units/test_nethack_taskset.py +213 -0
  80. synth_ai/environments/examples/nethack/units/test_recording.py +307 -0
  81. synth_ai/environments/examples/red/__init__.py +7 -0
  82. synth_ai/environments/examples/red/agent_demos/__init__.py +1 -0
  83. synth_ai/environments/examples/red/agent_demos/test_synth_react.py +1471 -0
  84. synth_ai/environments/examples/red/config_logging.py +110 -0
  85. synth_ai/environments/examples/red/engine.py +693 -0
  86. synth_ai/environments/examples/red/engine_helpers/__init__.py +1 -0
  87. synth_ai/environments/examples/red/engine_helpers/memory_map.py +28 -0
  88. synth_ai/environments/examples/red/engine_helpers/reward_components.py +275 -0
  89. synth_ai/environments/examples/red/engine_helpers/reward_library/__init__.py +142 -0
  90. synth_ai/environments/examples/red/engine_helpers/reward_library/adaptive_rewards.py +56 -0
  91. synth_ai/environments/examples/red/engine_helpers/reward_library/battle_rewards.py +283 -0
  92. synth_ai/environments/examples/red/engine_helpers/reward_library/composite_rewards.py +149 -0
  93. synth_ai/environments/examples/red/engine_helpers/reward_library/economy_rewards.py +137 -0
  94. synth_ai/environments/examples/red/engine_helpers/reward_library/efficiency_rewards.py +56 -0
  95. synth_ai/environments/examples/red/engine_helpers/reward_library/exploration_rewards.py +330 -0
  96. synth_ai/environments/examples/red/engine_helpers/reward_library/novelty_rewards.py +120 -0
  97. synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_rewards.py +558 -0
  98. synth_ai/environments/examples/red/engine_helpers/reward_library/pokemon_rewards.py +312 -0
  99. synth_ai/environments/examples/red/engine_helpers/reward_library/social_rewards.py +147 -0
  100. synth_ai/environments/examples/red/engine_helpers/reward_library/story_rewards.py +246 -0
  101. synth_ai/environments/examples/red/engine_helpers/screen_analysis.py +367 -0
  102. synth_ai/environments/examples/red/engine_helpers/state_extraction.py +139 -0
  103. synth_ai/environments/examples/red/environment.py +235 -0
  104. synth_ai/environments/examples/red/taskset.py +77 -0
  105. synth_ai/environments/examples/red/test_fixes.py +125 -0
  106. synth_ai/environments/examples/red/test_fixes_mock.py +148 -0
  107. synth_ai/environments/examples/red/units/__init__.py +1 -0
  108. synth_ai/environments/examples/red/units/test_basic_functionality.py +97 -0
  109. synth_ai/environments/examples/red/units/test_button_press_requirements.py +217 -0
  110. synth_ai/environments/examples/red/units/test_engine.py +192 -0
  111. synth_ai/environments/examples/red/units/test_environment.py +455 -0
  112. synth_ai/environments/examples/red/units/test_exploration_strategy.py +227 -0
  113. synth_ai/environments/examples/red/units/test_integration.py +217 -0
  114. synth_ai/environments/examples/red/units/test_memory_extraction.py +111 -0
  115. synth_ai/environments/examples/red/units/test_menu_bug_reproduction.py +1100 -0
  116. synth_ai/environments/examples/red/units/test_movement_debug.py +255 -0
  117. synth_ai/environments/examples/red/units/test_pokemon_mcts_debug.py +163 -0
  118. synth_ai/environments/examples/red/units/test_pokemon_mcts_verbose.py +117 -0
  119. synth_ai/environments/examples/red/units/test_red_basic.py +145 -0
  120. synth_ai/environments/examples/red/units/test_red_comprehensive.py +323 -0
  121. synth_ai/environments/examples/red/units/test_retry_movement.py +195 -0
  122. synth_ai/environments/examples/red/units/test_reward_components.py +186 -0
  123. synth_ai/environments/examples/red/units/test_rom_integration.py +260 -0
  124. synth_ai/environments/examples/red/units/test_taskset.py +116 -0
  125. synth_ai/environments/examples/red/units/test_tree.py +448 -0
  126. synth_ai/environments/examples/sokoban/__init__.py +1 -0
  127. synth_ai/environments/examples/sokoban/agent_demos/sokoban_full_eval.py +900 -0
  128. synth_ai/environments/examples/sokoban/agent_demos/test_dspy_react.py +1 -0
  129. synth_ai/environments/examples/sokoban/agent_demos/test_sokoban_react_agent.py +498 -0
  130. synth_ai/environments/examples/sokoban/agent_demos/test_synth_lats.py +1 -0
  131. synth_ai/environments/examples/sokoban/agent_demos/test_synth_react_locally.py +748 -0
  132. synth_ai/environments/examples/sokoban/agent_demos/test_synth_react_service.py +296 -0
  133. synth_ai/environments/examples/sokoban/engine.py +675 -0
  134. synth_ai/environments/examples/sokoban/engine_helpers/__init__.py +1 -0
  135. synth_ai/environments/examples/sokoban/engine_helpers/room_utils.py +656 -0
  136. synth_ai/environments/examples/sokoban/engine_helpers/vendored/__init__.py +17 -0
  137. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/__init__.py +3 -0
  138. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/boxoban_env.py +129 -0
  139. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/render_utils.py +370 -0
  140. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/room_utils.py +331 -0
  141. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env.py +305 -0
  142. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_fixed_targets.py +66 -0
  143. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_pull.py +114 -0
  144. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_two_player.py +122 -0
  145. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_variations.py +394 -0
  146. synth_ai/environments/examples/sokoban/environment.py +228 -0
  147. synth_ai/environments/examples/sokoban/generate_verified_puzzles.py +438 -0
  148. synth_ai/environments/examples/sokoban/puzzle_loader.py +311 -0
  149. synth_ai/environments/examples/sokoban/taskset.py +425 -0
  150. synth_ai/environments/examples/sokoban/units/astar_common.py +94 -0
  151. synth_ai/environments/examples/sokoban/units/test_building_task_set.py +49 -0
  152. synth_ai/environments/examples/sokoban/units/test_false_positive.py +120 -0
  153. synth_ai/environments/examples/sokoban/units/test_simple_run_through_environment.py +119 -0
  154. synth_ai/environments/examples/sokoban/units/test_sokoban_environment.py +98 -0
  155. synth_ai/environments/examples/sokoban/units/test_tree.py +364 -0
  156. synth_ai/environments/examples/tictactoe/__init__.py +1 -0
  157. synth_ai/environments/examples/tictactoe/agent_demos/test_synth_react.py +266 -0
  158. synth_ai/environments/examples/tictactoe/agent_demos/test_tictactoe_react_agent.py +470 -0
  159. synth_ai/environments/examples/tictactoe/engine.py +368 -0
  160. synth_ai/environments/examples/tictactoe/environment.py +239 -0
  161. synth_ai/environments/examples/tictactoe/taskset.py +214 -0
  162. synth_ai/environments/examples/tictactoe/units/test_tictactoe_engine.py +393 -0
  163. synth_ai/environments/examples/tictactoe/units/test_tictactoe_environment.py +493 -0
  164. synth_ai/environments/examples/tictactoe/units/test_tictactoe_taskset.py +191 -0
  165. synth_ai/environments/examples/verilog/__init__.py +10 -0
  166. synth_ai/environments/examples/verilog/agent_demos/test_synth_react.py +520 -0
  167. synth_ai/environments/examples/verilog/engine.py +328 -0
  168. synth_ai/environments/examples/verilog/environment.py +349 -0
  169. synth_ai/environments/examples/verilog/taskset.py +418 -0
  170. synth_ai/environments/examples/verilog/units/test_verilog_engine.py +466 -0
  171. synth_ai/environments/examples/verilog/units/test_verilog_environment.py +585 -0
  172. synth_ai/environments/examples/verilog/units/test_verilog_integration.py +383 -0
  173. synth_ai/environments/examples/verilog/units/test_verilog_taskset.py +457 -0
  174. synth_ai/environments/reproducibility/core.py +42 -0
  175. synth_ai/environments/reproducibility/tree.py +364 -0
  176. synth_ai/environments/service/app.py +78 -0
  177. synth_ai/environments/service/core_routes.py +775 -0
  178. synth_ai/environments/service/external_registry.py +57 -0
  179. synth_ai/environments/service/registry.py +9 -0
  180. synth_ai/environments/stateful/__init__.py +1 -0
  181. synth_ai/environments/stateful/core.py +28 -0
  182. synth_ai/environments/stateful/engine.py +21 -0
  183. synth_ai/environments/stateful/state.py +7 -0
  184. synth_ai/environments/tasks/api.py +19 -0
  185. synth_ai/environments/tasks/core.py +78 -0
  186. synth_ai/environments/tasks/filters.py +39 -0
  187. synth_ai/environments/tasks/utils.py +89 -0
  188. synth_ai/environments/v0_observability/history.py +3 -0
  189. synth_ai/environments/v0_observability/log.py +2 -0
  190. synth_ai/lm/caching/constants.py +1 -0
  191. synth_ai/{zyk/lms → lm}/caching/ephemeral.py +4 -8
  192. synth_ai/{zyk/lms → lm}/caching/handler.py +15 -15
  193. synth_ai/{zyk/lms → lm}/caching/initialize.py +2 -4
  194. synth_ai/{zyk/lms → lm}/caching/persistent.py +4 -10
  195. synth_ai/{zyk/lms → lm}/config.py +2 -1
  196. synth_ai/{zyk/lms → lm}/constants.py +2 -2
  197. synth_ai/{zyk/lms → lm}/core/all.py +10 -10
  198. synth_ai/{zyk/lms → lm}/core/main.py +57 -33
  199. synth_ai/{zyk/lms → lm}/core/vendor_clients.py +12 -10
  200. synth_ai/lm/cost/monitor.py +1 -0
  201. synth_ai/lm/cost/statefulness.py +1 -0
  202. synth_ai/lm/provider_support/__init__.py +8 -0
  203. synth_ai/lm/provider_support/anthropic.py +945 -0
  204. synth_ai/lm/provider_support/openai.py +1115 -0
  205. synth_ai/lm/provider_support/suppress_logging.py +31 -0
  206. synth_ai/{zyk/lms → lm}/structured_outputs/handler.py +58 -80
  207. synth_ai/{zyk/lms → lm}/structured_outputs/inject.py +6 -20
  208. synth_ai/{zyk/lms → lm}/structured_outputs/rehabilitate.py +6 -12
  209. synth_ai/{zyk/lms → lm}/vendors/core/anthropic_api.py +21 -30
  210. synth_ai/{zyk/lms → lm}/vendors/core/gemini_api.py +37 -32
  211. synth_ai/{zyk/lms → lm}/vendors/core/mistral_api.py +19 -28
  212. synth_ai/{zyk/lms → lm}/vendors/core/openai_api.py +26 -36
  213. synth_ai/{zyk/lms → lm}/vendors/openai_standard.py +29 -33
  214. synth_ai/{zyk/lms → lm}/vendors/retries.py +1 -1
  215. synth_ai/lm/vendors/supported/__init__.py +0 -0
  216. synth_ai/{zyk/lms → lm}/vendors/supported/custom_endpoint.py +131 -118
  217. synth_ai/{zyk/lms → lm}/vendors/supported/deepseek.py +4 -8
  218. synth_ai/{zyk/lms → lm}/vendors/supported/grok.py +6 -8
  219. synth_ai/{zyk/lms → lm}/vendors/supported/groq.py +1 -1
  220. synth_ai/{zyk/lms → lm}/vendors/supported/ollama.py +2 -2
  221. synth_ai/{zyk/lms → lm}/vendors/supported/openrouter.py +18 -16
  222. synth_ai/{zyk/lms → lm}/vendors/supported/together.py +1 -1
  223. synth_ai/tracing/__init__.py +0 -0
  224. synth_ai/tracing/abstractions.py +224 -0
  225. synth_ai/tracing/base_client.py +91 -0
  226. synth_ai/tracing/client_manager.py +131 -0
  227. synth_ai/tracing/config.py +140 -0
  228. synth_ai/tracing/context.py +146 -0
  229. synth_ai/tracing/decorators.py +679 -0
  230. synth_ai/tracing/events/__init__.py +0 -0
  231. synth_ai/tracing/events/manage.py +147 -0
  232. synth_ai/tracing/events/scope.py +86 -0
  233. synth_ai/tracing/events/store.py +227 -0
  234. synth_ai/tracing/immediate_client.py +152 -0
  235. synth_ai/tracing/local.py +18 -0
  236. synth_ai/tracing/log_client_base.py +74 -0
  237. synth_ai/tracing/retry_queue.py +187 -0
  238. synth_ai/tracing/trackers.py +515 -0
  239. synth_ai/tracing/upload.py +504 -0
  240. synth_ai/tracing/utils.py +9 -0
  241. synth_ai/zyk/__init__.py +28 -2
  242. synth_ai-0.2.1.dev0.dist-info/METADATA +349 -0
  243. synth_ai-0.2.1.dev0.dist-info/RECORD +261 -0
  244. synth_ai/zyk/lms/caching/constants.py +0 -1
  245. synth_ai/zyk/lms/cost/monitor.py +0 -1
  246. synth_ai/zyk/lms/cost/statefulness.py +0 -1
  247. synth_ai-0.1.9.dist-info/METADATA +0 -37
  248. synth_ai-0.1.9.dist-info/RECORD +0 -50
  249. /synth_ai/{zyk/lms/__init__.py → environments/reproducibility/helpers.py} +0 -0
  250. /synth_ai/{zyk/lms/caching → lm}/__init__.py +0 -0
  251. /synth_ai/{zyk/lms/core → lm/caching}/__init__.py +0 -0
  252. /synth_ai/{zyk/lms → lm}/caching/dbs.py +0 -0
  253. /synth_ai/{zyk/lms/cost → lm/core}/__init__.py +0 -0
  254. /synth_ai/{zyk/lms → lm}/core/exceptions.py +0 -0
  255. /synth_ai/{zyk/lms/structured_outputs → lm/cost}/__init__.py +0 -0
  256. /synth_ai/{zyk/lms/vendors → lm/structured_outputs}/__init__.py +0 -0
  257. /synth_ai/{zyk/lms → lm}/tools/__init__.py +0 -0
  258. /synth_ai/{zyk/lms → lm}/tools/base.py +0 -0
  259. /synth_ai/{zyk/lms/vendors/core → lm/vendors}/__init__.py +0 -0
  260. /synth_ai/{zyk/lms → lm}/vendors/base.py +0 -0
  261. /synth_ai/{zyk/lms/vendors/local → lm/vendors/core}/__init__.py +0 -0
  262. /synth_ai/{zyk/lms/vendors/supported → lm/vendors/local}/__init__.py +0 -0
  263. /synth_ai/{zyk/lms → lm}/vendors/local/ollama.py +0 -0
  264. {synth_ai-0.1.9.dist-info → synth_ai-0.2.1.dev0.dist-info}/WHEEL +0 -0
  265. {synth_ai-0.1.9.dist-info → synth_ai-0.2.1.dev0.dist-info}/licenses/LICENSE +0 -0
  266. {synth_ai-0.1.9.dist-info → synth_ai-0.2.1.dev0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,425 @@
1
+ from synth_ai.environments.tasks.core import (
2
+ Task,
3
+ TaskInstance,
4
+ TaskInstanceMetadata,
5
+ TaskInstanceMetadataFilter,
6
+ TaskInstanceSet,
7
+ )
8
+ from uuid import uuid4, UUID
9
+ from synth_ai.environments.tasks.core import SplitInfo, Impetus, Intent
10
+ from synth_ai.environments.examples.sokoban.puzzle_loader import (
11
+ get_puzzle_loader,
12
+ SokobanPuzzle,
13
+ )
14
+ from dataclasses import dataclass, asdict, fields
15
+ from typing import Tuple, List
16
+ import os
17
+ import logging
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ sokoban_task = Task(
22
+ global_premises="Procedural Sokoban task generation",
23
+ global_constraints="",
24
+ global_objectives="Push all boxes onto target locations",
25
+ shared_env_params={},
26
+ )
27
+
28
+ # Configuration parameters
29
+ NUM_INSTANCES_PER_DIFFICULTY = 10 # Number of puzzles to include per difficulty in the taskset
30
+ DIFFICULTY_CONFIGS = {
31
+ "ultra_easy": {
32
+ "impetus_prompt": "Solve this very simple Sokoban puzzle by pushing the box onto the target.",
33
+ },
34
+ "easy": {
35
+ "impetus_prompt": "Solve this simple Sokoban puzzle by pushing the box onto the target.",
36
+ },
37
+ "medium": {
38
+ "impetus_prompt": "Solve this Sokoban puzzle by pushing the 2 boxes onto the targets.",
39
+ },
40
+ "hard": {
41
+ "impetus_prompt": "Solve this challenging Sokoban puzzle by pushing the 3 boxes onto the targets.",
42
+ },
43
+ }
44
+
45
+
46
+ @dataclass
47
+ class SokobanTaskInstanceMetadata(TaskInstanceMetadata):
48
+ difficulty: str
49
+ num_boxes: int
50
+ dim_room: Tuple[int, int]
51
+ max_steps: int
52
+ shortest_path_length: int
53
+ seed: int
54
+ generation_params: str
55
+
56
+
57
+ @dataclass
58
+ class SokobanTaskInstance(TaskInstance):
59
+ async def serialize(self) -> dict:
60
+ data = asdict(self)
61
+ if "id" in data and isinstance(data["id"], UUID):
62
+ data["id"] = str(data["id"])
63
+ if "intent" in data and data["intent"] is not None:
64
+ if "deterministic_eval_functions" in data["intent"]:
65
+ data["intent"]["deterministic_eval_functions"] = []
66
+ return data
67
+
68
+ @classmethod
69
+ async def deserialize(cls, data: dict) -> "SokobanTaskInstance":
70
+ """Gracefully accept non-UUID ids (e.g. 'demo-mcts')."""
71
+ if "id" in data:
72
+ try:
73
+ data["id"] = UUID(str(data["id"]))
74
+ except (ValueError, TypeError, AttributeError):
75
+ pass # keep original string
76
+
77
+ if "impetus" in data and isinstance(data["impetus"], dict):
78
+ data["impetus"] = Impetus(**data["impetus"])
79
+
80
+ if "intent" in data and isinstance(data["intent"], dict):
81
+ intent_data = data["intent"]
82
+ intent_data["deterministic_eval_functions"] = []
83
+ if "gold_trajectories" in intent_data and intent_data["gold_trajectories"] is not None:
84
+ pass
85
+ data["intent"] = Intent(**intent_data)
86
+
87
+ if "metadata" in data and isinstance(data["metadata"], dict):
88
+ data["metadata"] = SokobanTaskInstanceMetadata(**data["metadata"])
89
+
90
+ constructor_field_names = {f.name for f in fields(cls)}
91
+ filtered_data = {k: v for k, v in data.items() if k in constructor_field_names}
92
+
93
+ return cls(**filtered_data)
94
+
95
+
96
+ async def create_sokoban_taskset() -> TaskInstanceSet:
97
+ """Generates Sokoban task instances from pre-generated verified puzzles."""
98
+ instances = []
99
+
100
+ # Load pre-generated puzzles
101
+ try:
102
+ puzzle_loader = get_puzzle_loader()
103
+ logger.info("Loading pre-generated Sokoban puzzles...")
104
+ puzzle_loader.load_puzzles()
105
+ logger.info(f"Loaded {puzzle_loader.get_total_puzzle_count()} total puzzles")
106
+ except Exception as e:
107
+ logger.error(f"Failed to load pre-generated puzzles: {e}")
108
+ logger.info("Falling back to empty taskset. Run generate_verified_puzzles.py first.")
109
+ return TaskInstanceSet(
110
+ name="Sokoban Verified TaskSet",
111
+ description="Verified pre-generated Sokoban tasks with guaranteed solvability.",
112
+ instances=[],
113
+ split_info=SplitInfo(
114
+ val_instance_ids=set(), test_instance_ids=set(), _is_split_defined=True
115
+ ),
116
+ )
117
+
118
+ for difficulty, config in DIFFICULTY_CONFIGS.items():
119
+ available_puzzles = puzzle_loader.get_puzzles_by_difficulty(difficulty)
120
+
121
+ if not available_puzzles:
122
+ logger.warning(f"No puzzles found for difficulty {difficulty}")
123
+ continue
124
+
125
+ # Take up to NUM_INSTANCES_PER_DIFFICULTY puzzles
126
+ puzzles_to_use = available_puzzles[:NUM_INSTANCES_PER_DIFFICULTY]
127
+ logger.info(f"Using {len(puzzles_to_use)} puzzles for {difficulty} difficulty")
128
+
129
+ for puzzle in puzzles_to_use:
130
+ instance_id = uuid4()
131
+
132
+ impetus = Impetus(instructions=config["impetus_prompt"])
133
+ intent = Intent(
134
+ rubric={"goal": "Push all boxes onto target locations."},
135
+ gold_trajectories=None,
136
+ gold_state_diff={},
137
+ )
138
+ metadata = SokobanTaskInstanceMetadata(
139
+ difficulty=difficulty,
140
+ num_boxes=puzzle.num_boxes,
141
+ dim_room=puzzle.dim_room,
142
+ max_steps=puzzle.max_steps,
143
+ shortest_path_length=puzzle.solution_length,
144
+ seed=puzzle.generation_seed,
145
+ generation_params=f"verified_puzzle_id={puzzle.id}",
146
+ )
147
+
148
+ # Use the puzzle data as the initial engine snapshot
149
+ initial_engine_snapshot = puzzle.to_engine_snapshot()
150
+
151
+ task_instance = SokobanTaskInstance(
152
+ id=instance_id,
153
+ impetus=impetus,
154
+ intent=intent,
155
+ metadata=metadata,
156
+ is_reproducible=True,
157
+ initial_engine_snapshot=initial_engine_snapshot,
158
+ )
159
+ instances.append(task_instance)
160
+
161
+ class NumBoxesFilter(TaskInstanceMetadataFilter):
162
+ def __init__(self, num_boxes):
163
+ self.num_boxes = num_boxes
164
+
165
+ def __call__(self, instance):
166
+ if hasattr(instance.metadata, "num_boxes"):
167
+ return instance.metadata.num_boxes == self.num_boxes
168
+ return False
169
+
170
+ class DimRoomFilter(TaskInstanceMetadataFilter):
171
+ def __init__(self, dim_room):
172
+ self.dim_room = dim_room
173
+
174
+ def __call__(self, instance):
175
+ if hasattr(instance.metadata, "dim_room"):
176
+ return instance.metadata.dim_room == self.dim_room
177
+ return False
178
+
179
+ class PathLengthFilter(TaskInstanceMetadataFilter):
180
+ def __init__(self, min_length=None, max_length=None):
181
+ self.min_length = min_length
182
+ self.max_length = max_length
183
+
184
+ def __call__(self, instance):
185
+ if not hasattr(instance.metadata, "shortest_path_length"):
186
+ return False
187
+ length = instance.metadata.shortest_path_length
188
+ if self.min_length is not None and length < self.min_length:
189
+ return False
190
+ if self.max_length is not None and length > self.max_length:
191
+ return False
192
+ return True
193
+
194
+ val_filter = NumBoxesFilter(2)
195
+ test_filter = PathLengthFilter(max_length=10)
196
+ val_ids = {inst.id for inst in instances if val_filter(inst)}
197
+ # remove anything already tagged as validation
198
+ test_ids = {inst.id for inst in instances if test_filter(inst) and inst.id not in val_ids}
199
+ split_info = SplitInfo(
200
+ val_instance_ids=val_ids,
201
+ test_instance_ids=test_ids,
202
+ _is_split_defined=True,
203
+ )
204
+
205
+ return TaskInstanceSet(
206
+ name="Sokoban Verified TaskSet",
207
+ description="Verified pre-generated Sokoban tasks with guaranteed solvability.",
208
+ instances=instances,
209
+ split_info=split_info,
210
+ )
211
+
212
+
213
+ async def create_easy_sokoban_taskset(num_instances: int = 50) -> TaskInstanceSet:
214
+ """Create a taskset with only easy difficulty puzzles."""
215
+ return await create_filtered_sokoban_taskset(
216
+ difficulties=["easy"], num_instances_per_difficulty=num_instances
217
+ )
218
+
219
+
220
+ async def create_filtered_sokoban_taskset(
221
+ difficulties: List[str], num_instances_per_difficulty: int = 10
222
+ ) -> TaskInstanceSet:
223
+ """
224
+ Create a taskset with only specified difficulties.
225
+
226
+ Args:
227
+ difficulties: List of difficulty levels to include
228
+ num_instances_per_difficulty: Number of instances per difficulty
229
+
230
+ Returns:
231
+ TaskInstanceSet with only the specified difficulties
232
+ """
233
+ instances = []
234
+
235
+ # Load pre-generated puzzles
236
+ try:
237
+ puzzle_loader = get_puzzle_loader()
238
+ logger.info("Loading pre-generated Sokoban puzzles...")
239
+ puzzle_loader.load_puzzles()
240
+ logger.info(f"Loaded {puzzle_loader.get_total_puzzle_count()} total puzzles")
241
+ except Exception as e:
242
+ logger.error(f"Failed to load pre-generated puzzles: {e}")
243
+ return TaskInstanceSet(
244
+ name="Sokoban Filtered TaskSet",
245
+ description=f"Filtered Sokoban tasks for difficulties: {', '.join(difficulties)}",
246
+ instances=[],
247
+ split_info=SplitInfo(
248
+ val_instance_ids=set(), test_instance_ids=set(), _is_split_defined=True
249
+ ),
250
+ )
251
+
252
+ for difficulty in difficulties:
253
+ if difficulty not in DIFFICULTY_CONFIGS:
254
+ logger.warning(f"Unknown difficulty '{difficulty}', skipping")
255
+ continue
256
+
257
+ config = DIFFICULTY_CONFIGS[difficulty]
258
+ available_puzzles = puzzle_loader.get_puzzles_by_difficulty(difficulty)
259
+
260
+ if not available_puzzles:
261
+ logger.warning(f"No puzzles found for difficulty {difficulty}")
262
+ continue
263
+
264
+ # Take up to num_instances_per_difficulty puzzles
265
+ puzzles_to_use = available_puzzles[:num_instances_per_difficulty]
266
+ logger.info(f"Using {len(puzzles_to_use)} puzzles for {difficulty} difficulty")
267
+
268
+ for puzzle in puzzles_to_use:
269
+ instance_id = uuid4()
270
+
271
+ impetus = Impetus(instructions=config["impetus_prompt"])
272
+ intent = Intent(
273
+ rubric={"goal": "Push all boxes onto target locations."},
274
+ gold_trajectories=None,
275
+ gold_state_diff={},
276
+ )
277
+ metadata = SokobanTaskInstanceMetadata(
278
+ difficulty=difficulty,
279
+ num_boxes=puzzle.num_boxes,
280
+ dim_room=puzzle.dim_room,
281
+ max_steps=puzzle.max_steps,
282
+ shortest_path_length=puzzle.solution_length,
283
+ seed=puzzle.generation_seed,
284
+ generation_params=f"verified_puzzle_id={puzzle.id}",
285
+ )
286
+
287
+ # Use the puzzle data as the initial engine snapshot
288
+ initial_engine_snapshot = puzzle.to_engine_snapshot()
289
+
290
+ task_instance = SokobanTaskInstance(
291
+ id=instance_id,
292
+ impetus=impetus,
293
+ intent=intent,
294
+ metadata=metadata,
295
+ is_reproducible=True,
296
+ initial_engine_snapshot=initial_engine_snapshot,
297
+ )
298
+ instances.append(task_instance)
299
+
300
+ # Create simple split info for filtered set
301
+ val_ids = {inst.id for inst in instances[::3]} # Every 3rd instance for validation
302
+ test_ids = {inst.id for inst in instances[1::3]} # Every 3rd starting from 1 for test
303
+ split_info = SplitInfo(
304
+ val_instance_ids=val_ids,
305
+ test_instance_ids=test_ids,
306
+ _is_split_defined=True,
307
+ )
308
+
309
+ return TaskInstanceSet(
310
+ name="Sokoban Filtered TaskSet",
311
+ description=f"Filtered Sokoban tasks for difficulties: {', '.join(difficulties)}",
312
+ instances=instances,
313
+ split_info=split_info,
314
+ )
315
+
316
+
317
+ async def create_task_instance_from_seed(difficulty: str, seed: int) -> SokobanTaskInstance:
318
+ """
319
+ Create a single task instance from a specific seed.
320
+ Uses modular arithmetic to deterministically select a puzzle.
321
+
322
+ Args:
323
+ difficulty: The difficulty level
324
+ seed: Seed for deterministic puzzle selection
325
+
326
+ Returns:
327
+ Single SokobanTaskInstance
328
+ """
329
+ from synth_ai.environments.examples.sokoban.puzzle_loader import get_puzzle_by_seed
330
+
331
+ puzzle = get_puzzle_by_seed(difficulty, seed)
332
+ if not puzzle:
333
+ raise ValueError(f"No puzzles available for difficulty '{difficulty}'")
334
+
335
+ config = DIFFICULTY_CONFIGS.get(difficulty)
336
+ if not config:
337
+ raise ValueError(f"Unknown difficulty '{difficulty}'")
338
+
339
+ instance_id = uuid4()
340
+
341
+ impetus = Impetus(instructions=config["impetus_prompt"])
342
+ intent = Intent(
343
+ rubric={"goal": "Push all boxes onto target locations."},
344
+ gold_trajectories=None,
345
+ gold_state_diff={},
346
+ )
347
+ metadata = SokobanTaskInstanceMetadata(
348
+ difficulty=difficulty,
349
+ num_boxes=puzzle.num_boxes,
350
+ dim_room=puzzle.dim_room,
351
+ max_steps=puzzle.max_steps,
352
+ shortest_path_length=puzzle.solution_length,
353
+ seed=seed, # Use the input seed, not the puzzle's generation seed
354
+ generation_params=f"verified_puzzle_id={puzzle.id}_from_seed={seed}",
355
+ )
356
+
357
+ # Use the puzzle data as the initial engine snapshot
358
+ initial_engine_snapshot = puzzle.to_engine_snapshot()
359
+
360
+ task_instance = SokobanTaskInstance(
361
+ id=instance_id,
362
+ impetus=impetus,
363
+ intent=intent,
364
+ metadata=metadata,
365
+ is_reproducible=True,
366
+ initial_engine_snapshot=initial_engine_snapshot,
367
+ )
368
+
369
+ return task_instance
370
+
371
+
372
+ # Example usage
373
+ if __name__ == "__main__":
374
+ import asyncio
375
+ import json
376
+ import os
377
+
378
+ NUM_INSTANCES_PER_DIFFICULTY = 2
379
+ # Updated path to examples/sokoban/dataset/instances.json
380
+ OUTPUT_FILE_PATH = "dataset/instances.json"
381
+
382
+ async def main():
383
+ taskset = await create_sokoban_taskset()
384
+
385
+ serialized = await asyncio.gather(*(inst.serialize() for inst in taskset.instances))
386
+
387
+ output_dir = os.path.dirname(OUTPUT_FILE_PATH)
388
+ if output_dir:
389
+ os.makedirs(output_dir, exist_ok=True)
390
+
391
+ with open(OUTPUT_FILE_PATH, "w") as f:
392
+ json.dump(serialized, f, indent=2)
393
+ print(f"Serialized {len(serialized)} instances to {OUTPUT_FILE_PATH}")
394
+
395
+ with open(OUTPUT_FILE_PATH, "r") as f:
396
+ read_serialized_data = json.load(f)
397
+
398
+ deserialized = await asyncio.gather(
399
+ *(SokobanTaskInstance.deserialize(data) for data in read_serialized_data)
400
+ )
401
+ print(f"Deserialized {len(deserialized)} instances.")
402
+
403
+ if any(inst is None for inst in deserialized):
404
+ print("Error: Deserialization returned None for some instances.")
405
+ for i, inst in enumerate(deserialized):
406
+ if inst is None:
407
+ print(
408
+ f"Instance at index {i} is None. Serialized data: {read_serialized_data[i]}"
409
+ )
410
+ return
411
+
412
+ val_ids = taskset.split_info.val_instance_ids
413
+ test_ids = taskset.split_info.test_instance_ids
414
+ all_ids = {inst.id for inst in deserialized}
415
+ train_ids = all_ids - val_ids - test_ids
416
+
417
+ train = [inst for inst in deserialized if inst.id in train_ids]
418
+ val = [inst for inst in deserialized if inst.id in val_ids]
419
+ test = [inst for inst in deserialized if inst.id in test_ids]
420
+
421
+ print(f"Train set ({len(train)} instances): {[str(i.id) for i in train]}")
422
+ print(f"Val set ({len(val)} instances): {[str(i.id) for i in val]}")
423
+ print(f"Test set ({len(test)} instances): {[str(i.id) for i in test]}")
424
+
425
+ asyncio.run(main())
@@ -0,0 +1,94 @@
1
+ """
2
+ astar_common.py – one A* routine usable by both engine-level and
3
+ environment-level unit tests.
4
+ """
5
+
6
+ import heapq
7
+ import itertools
8
+ import json
9
+ import numpy as np
10
+ from typing import List, Tuple, Callable, Awaitable, Any
11
+
12
+
13
+ # ---------- generic utilities ------------------------------------ #
14
+ def _boxes_left(env_pkg) -> int:
15
+ """#targets – #boxes-on-targets (uses raw grids, never the counter)."""
16
+ return int(np.sum(env_pkg.room_fixed == 2) - np.sum(env_pkg.room_state == 3))
17
+
18
+
19
+ def solved(obj: Any) -> bool:
20
+ """Expects obj to have a .package_sokoban_env attribute."""
21
+ return _boxes_left(obj.package_sokoban_env) == 0
22
+
23
+
24
+ def heuristic(obj: Any) -> int:
25
+ """Expects obj to have a .package_sokoban_env attribute."""
26
+ return _boxes_left(obj.package_sokoban_env)
27
+
28
+
29
+ # ---------- single reusable A* ----------------------------------- #
30
+ async def astar(
31
+ root_obj: Any,
32
+ step_fn: Callable[[Any, int], Awaitable[None]],
33
+ deserialize_fn: Callable[[Any], Awaitable[Any]],
34
+ max_nodes: int = 1000,
35
+ ) -> List[int]:
36
+ """
37
+ Generic A* over Sokoban snapshots.
38
+
39
+ • `root_obj` - current engine *or* environment
40
+ • `step_fn(obj, action)` - async: apply one move to *obj*
41
+ • `deserialize_fn(snapshot)` - async: new obj from snapshot
42
+ """
43
+ start_snap = await root_obj._serialize_engine()
44
+
45
+ frontier: List[Tuple[int, int, Any, List[int]]] = []
46
+ counter = itertools.count()
47
+ frontier.append((heuristic(root_obj), next(counter), start_snap, []))
48
+ seen: set[str] = set()
49
+
50
+ nodes = 0
51
+ while frontier and nodes < max_nodes:
52
+ f, _, snap, path = heapq.heappop(frontier)
53
+ cur = await deserialize_fn(snap)
54
+ key = json.dumps(snap.engine_snapshot, sort_keys=True)
55
+ if key in seen:
56
+ continue
57
+ seen.add(key)
58
+ if solved(cur):
59
+ return path
60
+
61
+ nodes += 1
62
+ for action in range(cur.package_sokoban_env.action_space.n):
63
+ child = await deserialize_fn(snap) # fresh copy
64
+ try:
65
+ await step_fn(child, action)
66
+ except Exception: # illegal/off-board
67
+ continue
68
+
69
+ child_snap = await child._serialize_engine()
70
+ g = len(path) + 1
71
+ heapq.heappush(
72
+ frontier,
73
+ (g + heuristic(child), next(counter), child_snap, path + [action]),
74
+ )
75
+ return []
76
+
77
+
78
+ # convenience lambdas for the two concrete APIs
79
+ async def _engine_step(e, a): # `SokobanEngine`
80
+ await e._step_engine(a)
81
+
82
+
83
+ async def _env_step(env, a): # `SokobanEnvironment` (expects Move wrapper)
84
+ from synth_ai.environments.examples.sokoban.units.test_sokoban_environment import Move
85
+
86
+ await env.step([[Move(a)]])
87
+
88
+
89
+ ENGINE_ASTAR = lambda eng, **kw: astar(eng, _engine_step, eng.__class__._deserialize_engine, **kw)
90
+ ENV_ASTAR = lambda env, **kw: astar(
91
+ env.engine, _env_step, env.engine.__class__._deserialize_engine, **kw
92
+ )
93
+
94
+ # ----------------------------------------------------------------- #
@@ -0,0 +1,49 @@
1
+ """
2
+ test_taskset_build.py – verifies create_sokoban_taskset().
3
+ """
4
+
5
+ import asyncio
6
+ import json
7
+ from pathlib import Path
8
+ from uuid import UUID
9
+
10
+ import pytest
11
+
12
+ from synth_ai.environments.examples.sokoban.taskset import (
13
+ create_sokoban_taskset,
14
+ SokobanTaskInstance,
15
+ )
16
+
17
+
18
+ @pytest.mark.asyncio
19
+ async def test_create_and_roundtrip_taskset(tmp_path: Path):
20
+ """
21
+ 1. build the task-set
22
+ 2. ensure splits are mutually exclusive & cover all ids
23
+ 3. serialize → disk → deserialize and check equality of id sets
24
+ """
25
+ ts = await create_sokoban_taskset()
26
+
27
+ # -------- split integrity -------- #
28
+ val = set(ts.split_info.val_instance_ids)
29
+ test = set(ts.split_info.test_instance_ids)
30
+ all_ids = {inst.id for inst in ts.instances}
31
+
32
+ # everything not explicitly in val or test is considered train
33
+ train = all_ids - val - test
34
+
35
+ # pair-wise disjointness checks
36
+ assert train.isdisjoint(val | test)
37
+ assert val.isdisjoint(test)
38
+ assert (train | val | test) == all_ids
39
+
40
+ # -------- round-trip serialisation -------- #
41
+ outfile = tmp_path / "instances.json"
42
+ serialised = await asyncio.gather(*(inst.serialize() for inst in ts.instances))
43
+ outfile.write_text(json.dumps(serialised))
44
+
45
+ loaded = json.loads(outfile.read_text())
46
+ deser = await asyncio.gather(*(SokobanTaskInstance.deserialize(d) for d in loaded))
47
+
48
+ deser_ids = {inst.id for inst in deser if isinstance(inst.id, UUID)}
49
+ assert deser_ids == all_ids
@@ -0,0 +1,120 @@
1
+ """
2
+ test_unsolvable_sokoban.py – make sure A* cannot "solve" an unsolvable level
3
+ and that the environment never reports a false positive.
4
+ """
5
+
6
+ import asyncio
7
+ import uuid
8
+ from typing import Dict, Any
9
+
10
+ import pytest
11
+
12
+ from synth_ai.environments.examples.sokoban.environment import SokobanEnvironment
13
+ from synth_ai.environments.examples.sokoban.units.astar_common import (
14
+ astar,
15
+ solved,
16
+ ) # buggy solved()!
17
+ from synth_ai.environments.examples.sokoban.taskset import (
18
+ SokobanTaskInstance,
19
+ SokobanTaskInstanceMetadata,
20
+ )
21
+ from synth_ai.environments.environment.tools import EnvToolCall
22
+ from synth_ai.environments.tasks.core import Impetus, Intent
23
+
24
+ # ───────────────────────────────── snapshot ───────────────────────────── #
25
+ UNSOLVABLE_SNAPSHOT: Dict[str, Any] = {
26
+ # # # # #
27
+ # _ X # #
28
+ # P # # #
29
+ # # # # #
30
+ "dim_room": [4, 4],
31
+ "room_fixed": [
32
+ [0, 0, 0, 0],
33
+ [0, 2, 1, 0],
34
+ [0, 1, 0, 0],
35
+ [0, 0, 0, 0],
36
+ ],
37
+ "room_state": [
38
+ [0, 0, 0, 0],
39
+ [0, 1, 4, 0],
40
+ [0, 5, 0, 0],
41
+ [0, 0, 0, 0],
42
+ ],
43
+ "boxes_on_target": 0,
44
+ "max_steps": 10,
45
+ "num_boxes": 1,
46
+ }
47
+
48
+
49
+ # ───────────────────────── helper object wrapper ──────────────────────── #
50
+ class Move(EnvToolCall): # type: ignore[misc]
51
+ def __init__(self, action: int):
52
+ self.action = action
53
+
54
+
55
+ # ──────────────────────────────── test ────────────────────────────────── #
56
+ @pytest.mark.asyncio
57
+ async def test_unsolvable_level_not_solved():
58
+ """A* should *not* find a solution and the env should *not* claim success."""
59
+ meta = SokobanTaskInstanceMetadata(
60
+ difficulty="unsolvable-unit",
61
+ num_boxes=1,
62
+ dim_room=(4, 4),
63
+ max_steps=10,
64
+ shortest_path_length=-1,
65
+ seed=-1,
66
+ generation_params="unsolvable-test",
67
+ )
68
+ ti = SokobanTaskInstance(
69
+ id=uuid.uuid4(),
70
+ impetus=Impetus(instructions="prove unsolvable"),
71
+ intent=Intent(rubric={}, gold_trajectories=None, gold_state_diff={}),
72
+ metadata=meta,
73
+ is_reproducible=True,
74
+ initial_engine_snapshot=UNSOLVABLE_SNAPSHOT,
75
+ )
76
+
77
+ env = SokobanEnvironment(ti)
78
+ await env.initialize()
79
+ env.engine.package_sokoban_env.observation_mode = "raw"
80
+
81
+ # ★ give the Environment the attribute the heuristic expects
82
+ env.package_sokoban_env = env.engine.package_sokoban_env # type: ignore[attr-defined]
83
+
84
+ root_snapshot = await env._serialize_engine()
85
+
86
+ async def custom_deserialize(snapshot: Any) -> SokobanEnvironment:
87
+ new_env = await SokobanEnvironment._deserialize_engine(snapshot)
88
+ new_env.package_sokoban_env = new_env.engine.package_sokoban_env # type: ignore[attr-defined]
89
+ return new_env
90
+
91
+ plan = await astar(
92
+ root_obj=env,
93
+ step_fn=lambda e, a: e.step([[Move(a)]]),
94
+ deserialize_fn=custom_deserialize, # Use the wrapper
95
+ max_nodes=500,
96
+ )
97
+
98
+ # ---------------- expected behaviour ---------------- #
99
+ # (1) Search should *not* find a plan.
100
+ assert not plan, f"A* unexpectedly found a plan: {plan}"
101
+
102
+ # (2) Even if a buggy plan exists, replay must not mark the puzzle solved.
103
+ # Re-initialize environment for replay to ensure a clean state
104
+ env_for_replay = await SokobanEnvironment._deserialize_engine(root_snapshot)
105
+ # ★ give the new Environment instance the attribute the heuristic expects
106
+ env_for_replay.package_sokoban_env = env_for_replay.engine.package_sokoban_env # type: ignore[attr-defined]
107
+
108
+ for action_code in plan or []:
109
+ await env_for_replay.step([[Move(action_code)]])
110
+
111
+ solved_after_replay = solved(env_for_replay) # Use the existing buggy solved()
112
+ assert not solved_after_replay, "Environment incorrectly reports a solved state"
113
+
114
+ # If the current code base still has the buggy `solved()` that checks the
115
+ # player instead of the boxes, this test will **fail** here — that's the
116
+ # signal to apply the earlier fixes.
117
+
118
+
119
+ if __name__ == "__main__":
120
+ asyncio.run(test_unsolvable_level_not_solved())