world-model-optimizer 0.2.0__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 (308) hide show
  1. llm_waterfall/LICENSE +21 -0
  2. llm_waterfall/__init__.py +53 -0
  3. llm_waterfall/adapters/__init__.py +36 -0
  4. llm_waterfall/adapters/anthropic.py +105 -0
  5. llm_waterfall/adapters/aws_mantle.py +47 -0
  6. llm_waterfall/adapters/azure_openai.py +71 -0
  7. llm_waterfall/adapters/base.py +51 -0
  8. llm_waterfall/adapters/bedrock.py +309 -0
  9. llm_waterfall/adapters/openai.py +130 -0
  10. llm_waterfall/classify.py +184 -0
  11. llm_waterfall/pricing.py +110 -0
  12. llm_waterfall/py.typed +0 -0
  13. llm_waterfall/types.py +295 -0
  14. llm_waterfall/waterfall.py +255 -0
  15. wmo/__init__.py +38 -0
  16. wmo/agents/__init__.py +7 -0
  17. wmo/agents/default.py +29 -0
  18. wmo/agents/meta.py +55 -0
  19. wmo/agents/optimizer.py +55 -0
  20. wmo/agents/project.py +928 -0
  21. wmo/cli/__init__.py +5 -0
  22. wmo/cli/agent_session.py +1123 -0
  23. wmo/cli/app.py +2489 -0
  24. wmo/cli/e2b_cmds.py +212 -0
  25. wmo/cli/eval_closed_loop.py +207 -0
  26. wmo/cli/harness_app.py +1147 -0
  27. wmo/cli/harness_distill.py +659 -0
  28. wmo/cli/hosted_session.py +880 -0
  29. wmo/cli/ingest_cmd.py +165 -0
  30. wmo/cli/model_roles.py +82 -0
  31. wmo/cli/platform_cmds.py +372 -0
  32. wmo/cli/route_app.py +274 -0
  33. wmo/cli/session_state.py +243 -0
  34. wmo/cli/ui.py +1107 -0
  35. wmo/cli/workspace_sync.py +504 -0
  36. wmo/config/__init__.py +60 -0
  37. wmo/config/card.py +129 -0
  38. wmo/config/config.py +367 -0
  39. wmo/config/dotenv.py +67 -0
  40. wmo/config/settings.py +128 -0
  41. wmo/config/store.py +177 -0
  42. wmo/conftest.py +19 -0
  43. wmo/connect/__init__.py +88 -0
  44. wmo/connect/apps.py +78 -0
  45. wmo/connect/brave.py +284 -0
  46. wmo/connect/connector.py +79 -0
  47. wmo/connect/credentials.py +164 -0
  48. wmo/connect/github.py +321 -0
  49. wmo/connect/google.py +627 -0
  50. wmo/connect/notion.py +790 -0
  51. wmo/connect/oauth.py +461 -0
  52. wmo/connect/slack.py +555 -0
  53. wmo/connect/store.py +199 -0
  54. wmo/connect/types.py +156 -0
  55. wmo/core/__init__.py +21 -0
  56. wmo/core/parsing.py +281 -0
  57. wmo/core/render.py +271 -0
  58. wmo/core/text.py +40 -0
  59. wmo/core/types.py +116 -0
  60. wmo/distill/__init__.py +14 -0
  61. wmo/distill/agents.py +140 -0
  62. wmo/distill/config.py +1006 -0
  63. wmo/distill/cost.py +437 -0
  64. wmo/distill/data.py +921 -0
  65. wmo/distill/deadlines.py +254 -0
  66. wmo/distill/fake_tinker.py +734 -0
  67. wmo/distill/gate.py +122 -0
  68. wmo/distill/loop.py +3499 -0
  69. wmo/distill/renderers.py +399 -0
  70. wmo/distill/rendering.py +620 -0
  71. wmo/distill/rollouts.py +726 -0
  72. wmo/distill/samples.py +195 -0
  73. wmo/distill/store.py +829 -0
  74. wmo/distill/teacher.py +714 -0
  75. wmo/distill/tokens.py +535 -0
  76. wmo/distill/tracking.py +552 -0
  77. wmo/distill/tripwire.py +411 -0
  78. wmo/distill/xtoken/byte_offsets.py +152 -0
  79. wmo/distill/xtoken/chunks.py +457 -0
  80. wmo/distill/xtoken/prompt_logprobs.py +475 -0
  81. wmo/distill/xtoken/teacher_render.py +346 -0
  82. wmo/engine/__init__.py +28 -0
  83. wmo/engine/autoconfig.py +367 -0
  84. wmo/engine/build.py +346 -0
  85. wmo/engine/demo.py +77 -0
  86. wmo/engine/eval_suites.py +245 -0
  87. wmo/engine/grounding.py +491 -0
  88. wmo/engine/knowledge.py +291 -0
  89. wmo/engine/loader.py +36 -0
  90. wmo/engine/play.py +92 -0
  91. wmo/engine/prompts.py +99 -0
  92. wmo/engine/replay.py +443 -0
  93. wmo/engine/reporting.py +58 -0
  94. wmo/engine/workspace.py +468 -0
  95. wmo/engine/world_model.py +568 -0
  96. wmo/env/__init__.py +22 -0
  97. wmo/env/base.py +121 -0
  98. wmo/env/closed_loop.py +229 -0
  99. wmo/env/episode.py +107 -0
  100. wmo/env/llm_agent.py +93 -0
  101. wmo/env/scenarios.py +73 -0
  102. wmo/evals/__init__.py +52 -0
  103. wmo/evals/agreement.py +110 -0
  104. wmo/evals/base.py +45 -0
  105. wmo/evals/closed_loop.py +480 -0
  106. wmo/evals/failover.py +96 -0
  107. wmo/evals/gold.py +127 -0
  108. wmo/evals/grid.py +394 -0
  109. wmo/evals/grid_plot.py +205 -0
  110. wmo/evals/harbor/__init__.py +27 -0
  111. wmo/evals/harbor/agent.py +573 -0
  112. wmo/evals/harbor/ctrf.py +171 -0
  113. wmo/evals/harbor/e2b_environment.py +587 -0
  114. wmo/evals/harbor/e2b_template_policy.py +144 -0
  115. wmo/evals/harbor/scorer.py +875 -0
  116. wmo/evals/harbor/tasks.py +140 -0
  117. wmo/evals/open_loop.py +194 -0
  118. wmo/evals/tasks.py +53 -0
  119. wmo/harness/__init__.py +51 -0
  120. wmo/harness/code_runtime.py +288 -0
  121. wmo/harness/create.py +1191 -0
  122. wmo/harness/delta.py +220 -0
  123. wmo/harness/doc.py +556 -0
  124. wmo/harness/e2b_ledger.py +342 -0
  125. wmo/harness/e2b_reap.py +476 -0
  126. wmo/harness/e2b_sandbox.py +350 -0
  127. wmo/harness/environment.py +35 -0
  128. wmo/harness/live_session.py +543 -0
  129. wmo/harness/mutate.py +343 -0
  130. wmo/harness/pi_e2b.py +1710 -0
  131. wmo/harness/pi_entry/entry.ts +268 -0
  132. wmo/harness/pi_entry/runner_frames.ts +92 -0
  133. wmo/harness/pi_entry/runner_live.ts +587 -0
  134. wmo/harness/pi_entry/runner_service.ts +270 -0
  135. wmo/harness/pi_entry/runner_stdio.ts +374 -0
  136. wmo/harness/pi_entry/runner_termination.ts +142 -0
  137. wmo/harness/pi_local.py +262 -0
  138. wmo/harness/pi_runtime.py +495 -0
  139. wmo/harness/pi_vendor.py +65 -0
  140. wmo/harness/population.py +509 -0
  141. wmo/harness/project_proposer.py +569 -0
  142. wmo/harness/proposer.py +977 -0
  143. wmo/harness/runner_link.py +619 -0
  144. wmo/harness/runtime.py +389 -0
  145. wmo/harness/scoring.py +247 -0
  146. wmo/harness/skills.py +116 -0
  147. wmo/harness/source_tree.py +319 -0
  148. wmo/harness/store.py +176 -0
  149. wmo/harness/tools.py +105 -0
  150. wmo/harness/vendor/manifest.sha256 +58 -0
  151. wmo/harness/vendor/pi-agent/CHANGELOG.md +556 -0
  152. wmo/harness/vendor/pi-agent/LICENSE +21 -0
  153. wmo/harness/vendor/pi-agent/README.md +488 -0
  154. wmo/harness/vendor/pi-agent/VENDOR.md +39 -0
  155. wmo/harness/vendor/pi-agent/docs/agent-harness.md +486 -0
  156. wmo/harness/vendor/pi-agent/docs/durable-harness.md +212 -0
  157. wmo/harness/vendor/pi-agent/docs/hooks.md +445 -0
  158. wmo/harness/vendor/pi-agent/docs/models.md +966 -0
  159. wmo/harness/vendor/pi-agent/docs/observability.md +376 -0
  160. wmo/harness/vendor/pi-agent/package.json +60 -0
  161. wmo/harness/vendor/pi-agent/src/agent-loop.ts +748 -0
  162. wmo/harness/vendor/pi-agent/src/agent.ts +575 -0
  163. wmo/harness/vendor/pi-agent/src/harness/agent-harness.ts +1029 -0
  164. wmo/harness/vendor/pi-agent/src/harness/compaction/branch-summarization.ts +261 -0
  165. wmo/harness/vendor/pi-agent/src/harness/compaction/compaction.ts +747 -0
  166. wmo/harness/vendor/pi-agent/src/harness/compaction/utils.ts +144 -0
  167. wmo/harness/vendor/pi-agent/src/harness/env/nodejs.ts +550 -0
  168. wmo/harness/vendor/pi-agent/src/harness/messages.ts +164 -0
  169. wmo/harness/vendor/pi-agent/src/harness/prompt-templates.ts +267 -0
  170. wmo/harness/vendor/pi-agent/src/harness/session/jsonl-repo.ts +177 -0
  171. wmo/harness/vendor/pi-agent/src/harness/session/jsonl-storage.ts +293 -0
  172. wmo/harness/vendor/pi-agent/src/harness/session/memory-repo.ts +50 -0
  173. wmo/harness/vendor/pi-agent/src/harness/session/memory-storage.ts +131 -0
  174. wmo/harness/vendor/pi-agent/src/harness/session/repo-utils.ts +51 -0
  175. wmo/harness/vendor/pi-agent/src/harness/session/session.ts +267 -0
  176. wmo/harness/vendor/pi-agent/src/harness/session/uuid.ts +54 -0
  177. wmo/harness/vendor/pi-agent/src/harness/skills.ts +375 -0
  178. wmo/harness/vendor/pi-agent/src/harness/system-prompt.ts +34 -0
  179. wmo/harness/vendor/pi-agent/src/harness/types.ts +836 -0
  180. wmo/harness/vendor/pi-agent/src/harness/utils/shell-output.ts +135 -0
  181. wmo/harness/vendor/pi-agent/src/harness/utils/truncate.ts +344 -0
  182. wmo/harness/vendor/pi-agent/src/index.ts +44 -0
  183. wmo/harness/vendor/pi-agent/src/node.ts +2 -0
  184. wmo/harness/vendor/pi-agent/src/proxy.ts +367 -0
  185. wmo/harness/vendor/pi-agent/src/types.ts +428 -0
  186. wmo/harness/vendor/pi-agent/test/agent-loop.test.ts +1351 -0
  187. wmo/harness/vendor/pi-agent/test/agent.test.ts +699 -0
  188. wmo/harness/vendor/pi-agent/test/e2e.test.ts +404 -0
  189. wmo/harness/vendor/pi-agent/test/harness/agent-harness-stream.test.ts +213 -0
  190. wmo/harness/vendor/pi-agent/test/harness/agent-harness.test.ts +608 -0
  191. wmo/harness/vendor/pi-agent/test/harness/compaction.test.ts +655 -0
  192. wmo/harness/vendor/pi-agent/test/harness/nodejs-env.test.ts +321 -0
  193. wmo/harness/vendor/pi-agent/test/harness/prompt-templates.test.ts +90 -0
  194. wmo/harness/vendor/pi-agent/test/harness/repo.test.ts +68 -0
  195. wmo/harness/vendor/pi-agent/test/harness/resource-formatting.test.ts +24 -0
  196. wmo/harness/vendor/pi-agent/test/harness/session-test-utils.ts +55 -0
  197. wmo/harness/vendor/pi-agent/test/harness/session-uuid.test.ts +50 -0
  198. wmo/harness/vendor/pi-agent/test/harness/session.test.ts +156 -0
  199. wmo/harness/vendor/pi-agent/test/harness/skills.test.ts +116 -0
  200. wmo/harness/vendor/pi-agent/test/harness/storage.test.ts +299 -0
  201. wmo/harness/vendor/pi-agent/test/harness/system-prompt.test.ts +66 -0
  202. wmo/harness/vendor/pi-agent/test/harness/truncate.test.ts +169 -0
  203. wmo/harness/vendor/pi-agent/test/scratch/simple.ts +72 -0
  204. wmo/harness/vendor/pi-agent/test/utils/calculate.ts +32 -0
  205. wmo/harness/vendor/pi-agent/test/utils/get-current-time.ts +46 -0
  206. wmo/harness/vendor/pi-agent/tsconfig.build.json +13 -0
  207. wmo/harness/vendor/pi-agent/vitest.config.ts +19 -0
  208. wmo/harness/vendor/pi-agent/vitest.harness.config.ts +28 -0
  209. wmo/harness/vendor/vendor_pi.sh +59 -0
  210. wmo/harness/workspace_patch.py +270 -0
  211. wmo/ingest/__init__.py +47 -0
  212. wmo/ingest/adapter.py +72 -0
  213. wmo/ingest/base.py +114 -0
  214. wmo/ingest/braintrust.py +339 -0
  215. wmo/ingest/detect.py +126 -0
  216. wmo/ingest/langfuse.py +291 -0
  217. wmo/ingest/langsmith.py +444 -0
  218. wmo/ingest/mastra.py +330 -0
  219. wmo/ingest/messages.py +170 -0
  220. wmo/ingest/normalize.py +679 -0
  221. wmo/ingest/otel_genai.py +69 -0
  222. wmo/ingest/otel_writer.py +100 -0
  223. wmo/ingest/phoenix.py +150 -0
  224. wmo/ingest/postgres.py +246 -0
  225. wmo/ingest/posthog.py +320 -0
  226. wmo/ingest/quality.py +28 -0
  227. wmo/ingest/stream.py +209 -0
  228. wmo/ingest/testdata/sample_otlp.json +60 -0
  229. wmo/ingest/testdata/sample_spans.jsonl +3 -0
  230. wmo/optimize/__init__.py +25 -0
  231. wmo/optimize/base.py +143 -0
  232. wmo/optimize/gepa.py +806 -0
  233. wmo/optimize/judge.py +262 -0
  234. wmo/optimize/judge_quality.py +359 -0
  235. wmo/optimize/knn.py +468 -0
  236. wmo/optimize/numeric.py +152 -0
  237. wmo/optimize/outcomes.py +103 -0
  238. wmo/optimize/policy.py +669 -0
  239. wmo/optimize/report.py +231 -0
  240. wmo/optimize/reward.py +129 -0
  241. wmo/optimize/routing.py +373 -0
  242. wmo/platform/__init__.py +6 -0
  243. wmo/platform/auth.py +115 -0
  244. wmo/platform/client.py +551 -0
  245. wmo/platform/credentials.py +126 -0
  246. wmo/platform/transfer.py +158 -0
  247. wmo/providers/__init__.py +40 -0
  248. wmo/providers/_bedrock_chat.py +155 -0
  249. wmo/providers/_openai_common.py +182 -0
  250. wmo/providers/_responses_common.py +472 -0
  251. wmo/providers/anthropic.py +134 -0
  252. wmo/providers/azure_openai.py +296 -0
  253. wmo/providers/base.py +300 -0
  254. wmo/providers/bedrock.py +312 -0
  255. wmo/providers/models.py +205 -0
  256. wmo/providers/openai.py +143 -0
  257. wmo/providers/openai_responses.py +240 -0
  258. wmo/providers/pool.py +170 -0
  259. wmo/providers/registry.py +73 -0
  260. wmo/providers/retry.py +151 -0
  261. wmo/providers/tinker.py +936 -0
  262. wmo/providers/waterfall.py +336 -0
  263. wmo/research/__init__.py +81 -0
  264. wmo/research/ablation.py +133 -0
  265. wmo/research/concurrency_plot.py +523 -0
  266. wmo/research/concurrency_run.py +240 -0
  267. wmo/research/concurrency_scaling.py +270 -0
  268. wmo/research/gepa_scaling.py +274 -0
  269. wmo/research/pipeline.py +198 -0
  270. wmo/research/scaling_split.py +82 -0
  271. wmo/research/scenario_fidelity.py +198 -0
  272. wmo/research/scenario_recovery.py +92 -0
  273. wmo/research/seed_stability.py +90 -0
  274. wmo/research/trace_scaling.py +348 -0
  275. wmo/retrieval/__init__.py +6 -0
  276. wmo/retrieval/embedders.py +105 -0
  277. wmo/retrieval/leakfree.py +52 -0
  278. wmo/retrieval/retriever.py +173 -0
  279. wmo/scenarios/__init__.py +58 -0
  280. wmo/scenarios/builder.py +152 -0
  281. wmo/scenarios/mining/__init__.py +27 -0
  282. wmo/scenarios/mining/clustering.py +171 -0
  283. wmo/scenarios/mining/facets.py +226 -0
  284. wmo/scenarios/mining/selection.py +220 -0
  285. wmo/scenarios/synthesis/__init__.py +6 -0
  286. wmo/scenarios/synthesis/scenario_set.py +63 -0
  287. wmo/scenarios/synthesis/synthesizer.py +85 -0
  288. wmo/scenarios/verification/__init__.py +17 -0
  289. wmo/scenarios/verification/judge.py +97 -0
  290. wmo/scenarios/verification/verify.py +135 -0
  291. wmo/serving/__init__.py +5 -0
  292. wmo/serving/builds.py +451 -0
  293. wmo/serving/chat.py +878 -0
  294. wmo/serving/endpoint_config.py +64 -0
  295. wmo/serving/savings.py +250 -0
  296. wmo/serving/server.py +553 -0
  297. wmo/serving/traces_source.py +206 -0
  298. wmo/telemetry.py +213 -0
  299. wmo/tracking/__init__.py +36 -0
  300. wmo/tracking/clock.py +24 -0
  301. wmo/tracking/metered.py +125 -0
  302. wmo/tracking/pricing.py +99 -0
  303. wmo/tracking/store.py +31 -0
  304. wmo/tracking/tracker.py +149 -0
  305. world_model_optimizer-0.2.0.dist-info/METADATA +203 -0
  306. world_model_optimizer-0.2.0.dist-info/RECORD +308 -0
  307. world_model_optimizer-0.2.0.dist-info/WHEEL +4 -0
  308. world_model_optimizer-0.2.0.dist-info/entry_points.txt +2 -0
wmo/env/base.py ADDED
@@ -0,0 +1,121 @@
1
+ """The `Env` protocol and its world-model backend.
2
+
3
+ An `Env` is one episode's worth of environment: `reset` starts it, `step` advances it. Real
4
+ environments (a benchmark harness, a coded oracle app, a simulator) implement the same protocol in
5
+ their example folders, which is what makes "iterate in the world model, validate in the real env"
6
+ a one-line swap instead of two agent loops.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Protocol, runtime_checkable
12
+
13
+ from wmo.core.types import Action, EnvState, Observation
14
+ from wmo.engine.world_model import WorldModel
15
+ from wmo.optimize.reward import EpisodeScore
16
+ from wmo.tracking import RunRecord
17
+
18
+
19
+ @runtime_checkable
20
+ class Env(Protocol):
21
+ """One episode of an environment an agent steps against."""
22
+
23
+ def reset(self, task: str | None = None, seed_state: EnvState | None = None) -> EnvState:
24
+ """Start a fresh episode; returns the environment's LIVE state view.
25
+
26
+ Contract: the returned `EnvState` is the env's current state object, updated in place as
27
+ the episode advances — callers that need a point-in-time snapshot must copy it
28
+ (`state.model_copy(deep=True)`), which is what `run_episode` does per recorded step.
29
+ """
30
+ ...
31
+
32
+ def step(self, action: Action) -> Observation:
33
+ """Apply `action` to the current episode and return the environment's response."""
34
+ ...
35
+
36
+ def close(self) -> None:
37
+ """Release episode resources (sessions, containers, sim handles). Idempotent."""
38
+ ...
39
+
40
+
41
+ class WorldModelEnv:
42
+ """`Env` backed by a `WorldModel` session.
43
+
44
+ Each `reset` opens a new session (ending any previous one); `step` delegates to
45
+ `WorldModel.step`. `close` ends the session in the world model — freeing its history and
46
+ metering — and keeps the final token/cost record available as `usage`.
47
+
48
+ RL rollouts need the episode judged before the session's history is freed, and `run_episode`
49
+ closes the env in its `finally` — so with `score_on_close=True`, `close` scores the session
50
+ (`WorldModel.score_session`) right before ending it and keeps the result as `last_score`:
51
+
52
+ env = WorldModelEnv(wm, score_on_close=True)
53
+ result = run_episode(env, agent, task)
54
+ reward = env.last_score.reward # scalar for GRPO/PPO/REINFORCE++; .critique for SDPO
55
+ """
56
+
57
+ def __init__(self, world_model: WorldModel, *, score_on_close: bool = False) -> None:
58
+ self._world_model = world_model
59
+ self._score_on_close = score_on_close
60
+ self._session_id: str | None = None
61
+ self._usage: RunRecord | None = None
62
+ self._last_score: EpisodeScore | None = None
63
+ self._score_error: Exception | None = None
64
+
65
+ @property
66
+ def session_id(self) -> str:
67
+ if self._session_id is None:
68
+ raise RuntimeError("WorldModelEnv has no active episode; call reset() first")
69
+ return self._session_id
70
+
71
+ @property
72
+ def usage(self) -> RunRecord | None:
73
+ """Token/cost/time of the current episode (live) or the last closed one (final)."""
74
+ if self._session_id is not None:
75
+ return self._world_model.session_usage(self._session_id)
76
+ return self._usage
77
+
78
+ @property
79
+ def last_score(self) -> EpisodeScore:
80
+ """The episode score captured by the most recent scoring `close`.
81
+
82
+ Raises if no scored episode has completed yet — either the env was built without
83
+ `score_on_close=True`, or `close` hasn't run. If the judge call itself failed during
84
+ `close` (throttle, network), the ORIGINAL failure is re-raised here: `run_episode`
85
+ deliberately swallows teardown errors, and a batch caller must see the real cause
86
+ instead of a misleading "no scored episode".
87
+ """
88
+ if self._score_error is not None:
89
+ raise RuntimeError("scoring failed during close; see cause") from self._score_error
90
+ if self._last_score is None:
91
+ raise RuntimeError(
92
+ "no scored episode yet; construct WorldModelEnv(wm, score_on_close=True) "
93
+ "and complete an episode (run_episode closes — and thus scores — for you)"
94
+ )
95
+ return self._last_score
96
+
97
+ def reset(self, task: str | None = None, seed_state: EnvState | None = None) -> EnvState:
98
+ self.close() # a leftover session would otherwise leak in the world model
99
+ session = self._world_model.new_session(task=task, seed_state=seed_state)
100
+ self._session_id = session.id
101
+ return session.state
102
+
103
+ def step(self, action: Action) -> Observation:
104
+ return self._world_model.step(self.session_id, action)
105
+
106
+ def close(self) -> None:
107
+ if self._session_id is None:
108
+ return
109
+ try:
110
+ if self._score_on_close:
111
+ self._last_score = None
112
+ self._score_error = None
113
+ try:
114
+ self._last_score = self._world_model.score_session(self._session_id)
115
+ except Exception as exc: # noqa: BLE001 - preserved and re-raised by last_score
116
+ # A judge failure must not leak the session or masquerade as "unscored";
117
+ # the session still ends below and last_score re-raises this cause.
118
+ self._score_error = exc
119
+ finally:
120
+ self._usage = self._world_model.end_session(self._session_id)
121
+ self._session_id = None
wmo/env/closed_loop.py ADDED
@@ -0,0 +1,229 @@
1
+ """Closed-loop evaluation: roll every pool candidate over a scenario set, collect the matrix.
2
+
3
+ Each (candidate model, scenario, episode) cell runs one `run_episode` with the candidate driving
4
+ `LLMAgent` against the env, then reads the env's episode score (VERIFY). The result is the
5
+ `OutcomeMatrix` the routing optimizer fits on and the improvement report cites.
6
+
7
+ Measurement notes:
8
+ - Latency is measured per POLICY CALL (the candidate's own completions), not per episode: episode
9
+ wall time is dominated by the world model's simulation latency, which production traffic never
10
+ pays, so quoting it would flatter nobody honestly.
11
+ - Cost is the candidate side only, priced by its own pool entry; the env's serve/judge cost is
12
+ metered separately by the world model (D12 cost split).
13
+ - Every raw candidate reply is stored (`ScenarioOutcome.replies`): that is the future
14
+ distillation feed. Providers do not yet surface separated thinking blocks; when they do, the
15
+ capture point is `_TimedProvider.complete`.
16
+ - A cell is recorded SCORED only when the episode also ran clean. An episode that errored
17
+ mid-flight (provider throttle, agent crash) is unscored even if the env still produced a
18
+ score, because that score grades a run the candidate never got to finish: counting it as
19
+ reward would read infrastructure failure as incapability. The salvaged critique is kept on
20
+ the row, prefixed, so the diagnostic text is not lost.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import hashlib
26
+ import logging
27
+ import time
28
+ from typing import TYPE_CHECKING, Protocol, cast
29
+
30
+ from wmo.env.base import Env
31
+ from wmo.env.episode import run_episode
32
+ from wmo.env.llm_agent import LLMAgent
33
+ from wmo.env.scenarios import Scenario
34
+ from wmo.optimize.outcomes import OutcomeMatrix, ScenarioOutcome
35
+ from wmo.optimize.reward import EpisodeScore
36
+ from wmo.providers.base import (
37
+ DEFAULT_MAX_TOKENS,
38
+ Completion,
39
+ Message,
40
+ Provider,
41
+ ProviderConfig,
42
+ TokenUsage,
43
+ VerifyResult,
44
+ )
45
+ from wmo.providers.pool import ModelPool, PoolEntry, pool_provider
46
+
47
+ if TYPE_CHECKING:
48
+ from collections.abc import Callable
49
+
50
+ logger = logging.getLogger(__name__)
51
+
52
+
53
+ class _TimedProvider:
54
+ """Wraps the candidate's provider to record per-call seconds, usage, and raw replies."""
55
+
56
+ def __init__(self, provider: Provider) -> None:
57
+ self._provider = provider
58
+ self.call_seconds: list[float] = []
59
+ self.replies: list[str] = []
60
+ self.usage = TokenUsage()
61
+
62
+ @property
63
+ def config(self) -> ProviderConfig:
64
+ return self._provider.config
65
+
66
+ def complete(
67
+ self,
68
+ system: str,
69
+ messages: list[Message],
70
+ *,
71
+ temperature: float = 0.7,
72
+ max_tokens: int = DEFAULT_MAX_TOKENS,
73
+ ) -> Completion:
74
+ started = time.monotonic()
75
+ completion = self._provider.complete(
76
+ system, messages, temperature=temperature, max_tokens=max_tokens
77
+ )
78
+ self.call_seconds.append(time.monotonic() - started)
79
+ self.replies.append(completion.text)
80
+ self.usage = TokenUsage(
81
+ input_tokens=self.usage.input_tokens + completion.usage.input_tokens,
82
+ cached_input_tokens=self.usage.cached_input_tokens
83
+ + completion.usage.cached_input_tokens,
84
+ output_tokens=self.usage.output_tokens + completion.usage.output_tokens,
85
+ )
86
+ return completion
87
+
88
+ def embed(self, texts: list[str]) -> list[list[float]]:
89
+ return self._provider.embed(texts)
90
+
91
+ def verify(self) -> VerifyResult:
92
+ return self._provider.verify()
93
+
94
+
95
+ _NEEDS_SCORING_ENV = (
96
+ "env produced no episode score; evaluate_pool needs a scoring env "
97
+ "(e.g. WorldModelEnv(world_model, score_on_close=True))"
98
+ )
99
+
100
+ # Prefix on a critique salvaged from an episode that errored: the text is diagnostic, the
101
+ # verdict is not evidence (see the module docstring).
102
+ _SALVAGE_PREFIX = "salvage-judged despite error: "
103
+
104
+
105
+ class _ScoringEnv(Protocol):
106
+ """The scoring surface `evaluate_pool` needs on top of `Env`; `WorldModelEnv` provides it."""
107
+
108
+ @property
109
+ def last_score(self) -> EpisodeScore: ...
110
+
111
+
112
+ def _read_episode_score(env: Env) -> tuple[EpisodeScore | None, str | None]:
113
+ """Read `env.last_score` defensively, returning (score, reason it is missing).
114
+
115
+ The two failure modes are NOT the same failure. A missing attribute means the caller handed
116
+ `evaluate_pool` a non-scoring env, so no cell of the sweep can ever be evidence and the
117
+ error is fatal. A `last_score` that RAISES (WorldModelEnv re-raises a close-time scoring
118
+ failure, e.g. a throttled judge call) is per-episode: that one cell is unscored with the
119
+ reason recorded, and the sweep keeps its other completed cells.
120
+ """
121
+ scoring = cast("_ScoringEnv", env)
122
+ try:
123
+ raw: object = scoring.last_score
124
+ except AttributeError as exc:
125
+ raise ValueError(_NEEDS_SCORING_ENV) from exc
126
+ except RuntimeError as exc:
127
+ return None, f"episode scoring failed: {exc}"
128
+ if raw is None:
129
+ return None, None
130
+ if not isinstance(raw, EpisodeScore):
131
+ # An unscored row must always say WHY (the outcomes contract); a wrong-typed score
132
+ # silently becoming reward=None/error=None would violate it.
133
+ return None, (
134
+ f"env last_score is {type(raw).__name__}, not EpisodeScore; episode left unscored"
135
+ )
136
+ return raw, None
137
+
138
+
139
+ def scenario_id(scenario: Scenario) -> str:
140
+ """Stable id for a scenario: its first provenance trace id, else a hash of the task.
141
+
142
+ Provisional until wm-create's generate contract ships first-class scenario ids
143
+ (DECISIONS.md 2026-07-23); both forms are deterministic across runs.
144
+ """
145
+ if scenario.provenance:
146
+ return scenario.provenance[0]
147
+ return hashlib.sha256(scenario.task.encode("utf-8")).hexdigest()[:12]
148
+
149
+
150
+ def evaluate_pool(
151
+ env_factory: Callable[[], Env],
152
+ pool: ModelPool,
153
+ scenarios: list[Scenario],
154
+ *,
155
+ episodes_per_scenario: int = 1,
156
+ max_steps: int = 20,
157
+ agent_temperature: float = 0.0,
158
+ tools_hint: str | None = None,
159
+ provider_factory: Callable[[PoolEntry], Provider] = pool_provider,
160
+ on_outcome: Callable[[ScenarioOutcome], None] | None = None,
161
+ ) -> OutcomeMatrix:
162
+ """Run every pool candidate over `scenarios`, one fresh env per episode.
163
+
164
+ The env must score episodes on close (`WorldModelEnv(..., score_on_close=True)`): a matrix
165
+ without verified rewards is not evidence. Episodes that error, and episodes whose scoring
166
+ itself fails, are recorded unscored (`reward=None`, `error` set) rather than defaulted to 0,
167
+ and never abort the sweep. `on_outcome` fires after each cell for progress display; a
168
+ callback that raises is logged and ignored, since a broken progress pipe must not throw away
169
+ the cells already paid for.
170
+ """
171
+ outcomes: list[ScenarioOutcome] = []
172
+ for entry in pool.models:
173
+ for scenario in scenarios:
174
+ sid = scenario_id(scenario)
175
+ for episode in range(episodes_per_scenario):
176
+ timed = _TimedProvider(provider_factory(entry))
177
+ agent = LLMAgent(timed, temperature=agent_temperature, tools_hint=tools_hint)
178
+ env = env_factory()
179
+ result = run_episode(env, agent, scenario.task, max_steps=max_steps)
180
+ score, score_error = _read_episode_score(env)
181
+ error = result.error
182
+ if score_error is not None:
183
+ error = f"{error}; {score_error}" if error else score_error
184
+ if score is None and error is None:
185
+ raise ValueError(_NEEDS_SCORING_ENV)
186
+ critique = score.critique if score else ""
187
+ if score is not None and result.error is not None:
188
+ # The episode broke mid-flight, so this verdict grades an unfinished run:
189
+ # keep the text, drop the reward (see the module docstring).
190
+ critique = f"{_SALVAGE_PREFIX}{critique}" if critique else ""
191
+ score = None
192
+ outcome = ScenarioOutcome(
193
+ scenario_id=sid,
194
+ task=scenario.task,
195
+ model=entry.name,
196
+ episode=episode,
197
+ reward=score.reward if score else None,
198
+ success=score.success if score else False,
199
+ critique=critique,
200
+ steps=len(result.steps),
201
+ stop_reason=str(result.stop_reason),
202
+ usage=timed.usage,
203
+ cost_usd=entry.cost_usd(timed.usage),
204
+ call_seconds=timed.call_seconds,
205
+ replies=timed.replies,
206
+ error=error,
207
+ )
208
+ outcomes.append(outcome)
209
+ if on_outcome is not None:
210
+ try:
211
+ on_outcome(outcome)
212
+ except Exception: # noqa: BLE001 - a broken progress pipe never costs cells
213
+ logger.warning(
214
+ "on_outcome callback failed for %s on %s ep%d; sweep continues",
215
+ entry.name,
216
+ sid,
217
+ episode,
218
+ exc_info=True,
219
+ )
220
+ logger.info(
221
+ "closed-loop %s on %s ep%d: reward=%s cost=$%.5f steps=%d",
222
+ entry.name,
223
+ sid,
224
+ episode,
225
+ "unscored" if outcome.reward is None else f"{outcome.reward:.2f}",
226
+ outcome.cost_usd,
227
+ outcome.steps,
228
+ )
229
+ return OutcomeMatrix(pool=pool.models, outcomes=outcomes)
wmo/env/episode.py ADDED
@@ -0,0 +1,107 @@
1
+ """`run_episode`: the one agent-vs-environment rollout loop.
2
+
3
+ Every workstream that "runs an agent against an environment for N steps and scores the result"
4
+ uses this loop, so episode records are comparable across the world model and real backends.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import traceback
10
+ from enum import StrEnum
11
+ from typing import Protocol, runtime_checkable
12
+
13
+ from pydantic import BaseModel, Field
14
+
15
+ from wmo.core.types import Action, ActionKind, EnvState, Step
16
+ from wmo.env.base import Env
17
+
18
+ # An agent signals it is finished by returning a MESSAGE action whose content equals this.
19
+ DONE_SIGNAL = "<DONE>"
20
+
21
+
22
+ @runtime_checkable
23
+ class Agent(Protocol):
24
+ """Anything that maps the episode so far to the next action.
25
+
26
+ `act` sees the task, the env's live state view (see `Env.reset`), and the full history of
27
+ steps taken this episode. Return an `Action` to continue, or a MESSAGE action whose content
28
+ is `DONE_SIGNAL` to stop.
29
+ """
30
+
31
+ def act(self, task: str | None, state: EnvState, history: list[Step]) -> Action: ...
32
+
33
+
34
+ class StopReason(StrEnum):
35
+ AGENT_DONE = "agent_done" # the agent returned DONE_SIGNAL
36
+ MAX_STEPS = "max_steps" # the step budget ran out
37
+ ENV_ERROR = "env_error" # env.step raised; episode recorded up to the failure
38
+ AGENT_ERROR = "agent_error" # agent.act raised; episode recorded up to the failure
39
+
40
+
41
+ class EpisodeResult(BaseModel):
42
+ """One completed rollout: what happened, why it stopped."""
43
+
44
+ task: str | None = None
45
+ steps: list[Step] = Field(default_factory=list)
46
+ stop_reason: StopReason
47
+ error: str | None = None # set on ENV_ERROR/AGENT_ERROR; names the failure
48
+ error_traceback: str | None = None # full traceback of that failure, for debugging batches
49
+
50
+
51
+ def run_episode(
52
+ env: Env,
53
+ agent: Agent,
54
+ task: str | None = None,
55
+ *,
56
+ seed_state: EnvState | None = None,
57
+ max_steps: int = 20,
58
+ ) -> EpisodeResult:
59
+ """Roll one episode of `agent` against `env`, bounded by `max_steps`.
60
+
61
+ The env's `reset`/`close` bracket the episode; each turn the agent proposes an action from the
62
+ accumulated history and the env answers with an observation. Each recorded step's
63
+ `state_before` is a deep copy of the env's state at that moment (the live state object keeps
64
+ mutating; see `Env.reset`). An env exception is recorded (not raised) so batch runs survive a
65
+ flaky backend; callers inspect `stop_reason`/`error`.
66
+ """
67
+ if max_steps < 1:
68
+ raise ValueError(f"max_steps must be >= 1, got {max_steps}")
69
+ try:
70
+ state = env.reset(task=task, seed_state=seed_state)
71
+ history: list[Step] = []
72
+ for _ in range(max_steps):
73
+ try:
74
+ action = agent.act(task, state, history)
75
+ except Exception as exc: # noqa: BLE001 - batch runs must survive one bad episode
76
+ return EpisodeResult(
77
+ task=task,
78
+ steps=history,
79
+ stop_reason=StopReason.AGENT_ERROR,
80
+ error=f"{type(exc).__name__}: {exc} (in agent.act)",
81
+ error_traceback="".join(traceback.format_exception(exc)),
82
+ )
83
+ if action.kind is ActionKind.MESSAGE and action.content == DONE_SIGNAL:
84
+ return EpisodeResult(task=task, steps=history, stop_reason=StopReason.AGENT_DONE)
85
+ state_before = state.model_copy(deep=True)
86
+ try:
87
+ observation = env.step(action)
88
+ except Exception as exc: # noqa: BLE001 - batch runs must survive one bad episode
89
+ return EpisodeResult(
90
+ task=task,
91
+ steps=history,
92
+ stop_reason=StopReason.ENV_ERROR,
93
+ error=(
94
+ f"{type(exc).__name__}: {exc} "
95
+ f"(while executing {action.kind.value} {action.name or action.content!r})"
96
+ ),
97
+ error_traceback="".join(traceback.format_exception(exc)),
98
+ )
99
+ history.append(
100
+ Step(action=action, observation=observation, state_before=state_before, task=task)
101
+ )
102
+ return EpisodeResult(task=task, steps=history, stop_reason=StopReason.MAX_STEPS)
103
+ finally:
104
+ try:
105
+ env.close()
106
+ except Exception: # noqa: BLE001, S110 - a teardown failure must not mask the result
107
+ pass
wmo/env/llm_agent.py ADDED
@@ -0,0 +1,93 @@
1
+ """A minimal LLM agent for rollouts: one tool call (or DONE) per turn, JSON-formatted.
2
+
3
+ This is the reusable counterpart of the throwaway agent inside `wmo demo`: it implements the
4
+ `Agent` protocol so scenario verification and research runs can roll real episodes against a
5
+ world model without every caller re-writing the same prompt-and-parse loop. It is deliberately
6
+ simple — no planning scaffold — because its role is "a competent baseline agent", not SOTA.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+
13
+ from pydantic import BaseModel, ValidationError
14
+
15
+ from wmo.core.parsing import extract_json_object
16
+ from wmo.core.types import Action, ActionKind, EnvState, JsonObject, Step
17
+ from wmo.env.episode import DONE_SIGNAL
18
+ from wmo.providers.base import Message, Provider
19
+
20
+ AGENT_SYSTEM = """You are an agent operating in a tool environment to complete a task.
21
+
22
+ Each turn, respond with ONLY a JSON object, no prose around it — one of:
23
+ {"tool": "<tool name>", "arguments": {...}} to act,
24
+ {"done": true, "summary": "<what you achieved>"} when the task is complete or impossible.
25
+
26
+ Choose tool names and arguments consistent with the environment's responses so far. Work
27
+ efficiently: no redundant calls, finish as soon as the task is done."""
28
+
29
+ _MAX_HISTORY_CHARS = 500
30
+
31
+
32
+ class _AgentReply(BaseModel):
33
+ """Lenient view of the agent's JSON reply."""
34
+
35
+ tool: str | None = None
36
+ arguments: JsonObject = {}
37
+ done: bool = False
38
+ summary: str = ""
39
+
40
+
41
+ class LLMAgent:
42
+ """`Agent`-protocol adapter around a provider: history in, one JSON tool call out."""
43
+
44
+ def __init__(
45
+ self, provider: Provider, *, temperature: float = 0.0, tools_hint: str | None = None
46
+ ) -> None:
47
+ self._provider = provider
48
+ self._temperature = temperature
49
+ # Corpus-derived tool surface (names + argument keys observed in the traces). Without
50
+ # it, capable models honestly refuse to invent tools while weaker ones hallucinate
51
+ # them, and closed-loop rewards measure affordance-guessing instead of capability.
52
+ self._system = AGENT_SYSTEM if not tools_hint else f"{AGENT_SYSTEM}\n\n{tools_hint}"
53
+
54
+ def act(self, task: str | None, state: EnvState, history: list[Step]) -> Action:
55
+ prompt = _render_turn(task, state, history)
56
+ completion = self._provider.complete(
57
+ self._system,
58
+ [Message(role="user", content=prompt)],
59
+ temperature=self._temperature,
60
+ max_tokens=1024,
61
+ )
62
+ raw = extract_json_object(completion.text)
63
+ if raw is not None:
64
+ try:
65
+ reply = _AgentReply.model_validate_json(raw)
66
+ except ValidationError:
67
+ reply = None
68
+ if reply is not None:
69
+ if reply.done or reply.tool is None:
70
+ return Action(kind=ActionKind.MESSAGE, content=DONE_SIGNAL)
71
+ return Action(kind=ActionKind.TOOL_CALL, name=reply.tool, arguments=reply.arguments)
72
+ # Unparseable reply: surface it as a message action; the env will answer and the episode
73
+ # continues rather than crashing the batch.
74
+ return Action(kind=ActionKind.MESSAGE, content=completion.text.strip()[:_MAX_HISTORY_CHARS])
75
+
76
+
77
+ def _render_turn(task: str | None, state: EnvState, history: list[Step]) -> str:
78
+ lines = [f"TASK: {task or '(none)'}"]
79
+ if state.scratchpad:
80
+ lines.append(f"ENVIRONMENT NOTES: {state.scratchpad}")
81
+ if history:
82
+ lines.append("EPISODE SO FAR:")
83
+ for index, step in enumerate(history):
84
+ action = step.action
85
+ if action.kind is ActionKind.TOOL_CALL:
86
+ call = f"{action.name}({json.dumps(action.arguments, default=str)})"
87
+ else:
88
+ call = f"message: {action.content}"
89
+ observation = step.observation.content[:_MAX_HISTORY_CHARS]
90
+ error_mark = " [ERROR]" if step.observation.is_error else ""
91
+ lines.append(f"{index}. {call} -> {observation}{error_mark}")
92
+ lines.append("Your next move (JSON only):")
93
+ return "\n".join(lines)
wmo/env/scenarios.py ADDED
@@ -0,0 +1,73 @@
1
+ """Scenarios: the task prompts an agent trains and is evaluated on, derived from traces.
2
+
3
+ v1 scenario creation is deliberately minimal (the prompts already recorded in the corpus's traces
4
+ ARE the scenarios): `scenarios_from_traces` extracts one `Scenario` per unique task from the given
5
+ traces. Callers control leakage by choosing which traces to pass — extract training scenarios from
6
+ the train split and held-out scenarios from the test split, and the two can never overlap because
7
+ the whole-trace split already separated them.
8
+
9
+ Principled scenario *generation* (coverage, difficulty calibration, counterfactuals) is a later
10
+ layer that will produce the same `Scenario` type.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from pydantic import BaseModel, Field
16
+
17
+ from wmo.core.types import ActionKind, Trace
18
+
19
+
20
+ class Scenario(BaseModel):
21
+ """One task an agent can attempt against an environment."""
22
+
23
+ task: str # the prompt handed to the agent (and to Env.reset)
24
+ provenance: list[str] = Field(default_factory=list) # trace_ids this scenario came from
25
+
26
+
27
+ def scenarios_from_traces(traces: list[Trace]) -> list[Scenario]:
28
+ """Extract the unique task prompts from `traces`, in first-seen order.
29
+
30
+ Traces without a task (or with a whitespace-only one) are skipped: a scenario is exactly "a
31
+ prompt we can hand to an agent", and an empty prompt isn't one. Duplicate tasks collapse into
32
+ a single scenario whose `provenance` lists every contributing trace.
33
+ """
34
+ by_task: dict[str, Scenario] = {}
35
+ for trace in traces:
36
+ task = _trace_task(trace)
37
+ if task is None:
38
+ continue
39
+ scenario = by_task.get(task)
40
+ if scenario is None:
41
+ by_task[task] = Scenario(task=task, provenance=[trace.trace_id])
42
+ elif trace.trace_id not in scenario.provenance:
43
+ scenario.provenance.append(trace.trace_id)
44
+ return list(by_task.values())
45
+
46
+
47
+ def _trace_task(trace: Trace) -> str | None:
48
+ """The trace's task prompt: first non-empty per-step task (steps carry it in this corpus)."""
49
+ for step in trace.steps:
50
+ if step.task and step.task.strip():
51
+ return step.task.strip()
52
+ return None
53
+
54
+
55
+ def tools_hint_from_traces(traces: list[Trace]) -> str:
56
+ """Summarize the corpus's observed tool surface for the rollout agent's system prompt.
57
+
58
+ One line per tool: name + the union of argument keys seen in the traces (the D28
59
+ tools.json derivation, inlined). Empty string for tool-less corpora. This is the
60
+ trace-derived stopgap for the scenario tool-surface contract: agents told what exists
61
+ stop being scored on their willingness to hallucinate affordances.
62
+ """
63
+ args_by_tool: dict[str, set[str]] = {}
64
+ for trace in traces:
65
+ for step in trace.steps:
66
+ action = step.action
67
+ if action.kind is not ActionKind.TOOL_CALL or not action.name:
68
+ continue
69
+ args_by_tool.setdefault(action.name, set()).update(action.arguments)
70
+ if not args_by_tool:
71
+ return ""
72
+ lines = [f"- {name}({', '.join(sorted(keys))})" for name, keys in sorted(args_by_tool.items())]
73
+ return "AVAILABLE TOOLS (observed in this environment):\n" + "\n".join(lines)
wmo/evals/__init__.py ADDED
@@ -0,0 +1,52 @@
1
+ """World-model evaluation: one interface, an open-loop and a closed-loop implementation.
2
+
3
+ - `base` — the general `Evaluation`/`EvalResult` interface: mode-specific inputs are bound at
4
+ construction; `run()` returns a report with a one-line `summary()` and a `headline` score.
5
+ - `open_loop` — teacher-forced replay of held-out trace steps, scored for per-step reconstruction
6
+ fidelity (the default `wmo eval` mode).
7
+ - `closed_loop` — a live agent runs tasks with the world model as its environment, gold-judged for
8
+ end-to-end task success over k=3 passes (`wmo eval --mode closed-loop`).
9
+ - `agreement` — compare two closed-loop reports task-by-task (e.g. simulated vs real): the
10
+ outcome-agreement validity check.
11
+ - `gold` / `tasks` — the gold-assertion judge and the task specs closed-loop eval scores against.
12
+ """
13
+
14
+ from wmo.evals.agreement import AgreementReport, compute_agreement
15
+ from wmo.evals.base import EvalResult, Evaluation
16
+ from wmo.evals.closed_loop import (
17
+ ClosedLoopEval,
18
+ ClosedLoopReport,
19
+ TaskOutcome,
20
+ WorldModelEnvironment,
21
+ evaluate_closed_loop,
22
+ evaluate_with_env,
23
+ )
24
+ from wmo.evals.gold import GoldJudge, GoldVerdict
25
+ from wmo.evals.grid import GridCell, GridResult, ModelSpec, merge_results, run_grid
26
+ from wmo.evals.open_loop import EvalReport, OpenLoopEval, evaluate_files
27
+ from wmo.evals.tasks import TaskSpec, load_tasks
28
+
29
+ __all__ = [
30
+ "AgreementReport",
31
+ "ClosedLoopEval",
32
+ "ClosedLoopReport",
33
+ "EvalReport",
34
+ "EvalResult",
35
+ "Evaluation",
36
+ "GoldJudge",
37
+ "GoldVerdict",
38
+ "GridCell",
39
+ "GridResult",
40
+ "ModelSpec",
41
+ "OpenLoopEval",
42
+ "TaskOutcome",
43
+ "TaskSpec",
44
+ "WorldModelEnvironment",
45
+ "compute_agreement",
46
+ "evaluate_closed_loop",
47
+ "evaluate_files",
48
+ "evaluate_with_env",
49
+ "load_tasks",
50
+ "merge_results",
51
+ "run_grid",
52
+ ]