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
@@ -0,0 +1,523 @@
1
+ """Render a concurrency scaling-law report to a brand-styled matplotlib figure.
2
+
3
+ `render_report` draws one benchmark's report (`wmo research concurrency --out`) as three panels:
4
+ batch wall-clock per side (endpoint seconds labelled), the headline T_real/T_world differential
5
+ (parity crossing shaded), and how each side parallelizes (speed-up vs ideal, log-log). The
6
+ cross-benchmark story is split into two standalone figures: `render_speedup` (the "what" — how many
7
+ times faster the world model is, per benchmark) and `render_cost` (the "why" — reconstruction vs.
8
+ real-setup cost). Each is a single clean panel with one left-aligned title (no subtitle). Styling
9
+ follows the brand system (AGENTS.md rule 15): white background, near-black ink, hairline grid,
10
+ brand-palette accents, left-aligned titles — matching plot_trace_scaling.py.
11
+
12
+ Needs the `viz` extra (matplotlib/pandas); it is imported lazily by the CLI so the harness runtime
13
+ has no plotting dependency. Kept out of `concurrency_scaling.py` so the core experiment stays
14
+ deployment-free and fake-testable.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from pathlib import Path
20
+
21
+ import matplotlib # noqa: E402 - grouped with the other plotting deps below
22
+ from pydantic import BaseModel, ValidationError
23
+
24
+ from wmo.research.concurrency_scaling import ConcurrencyScalingReport
25
+
26
+ matplotlib.use("Agg") # headless: write a file, never open a window
27
+ import matplotlib.pyplot as plt # noqa: E402 - must follow the Agg backend selection
28
+ import matplotlib.ticker as mticker # noqa: E402
29
+ import pandas as pd # noqa: E402
30
+
31
+
32
+ class _PlotRow(BaseModel):
33
+ """One tidy row for seaborn: a (level, side) timing point of a single report."""
34
+
35
+ level: int
36
+ side: str
37
+ wall: float
38
+ wall_std: float
39
+ speedup: float
40
+ efficiency: float
41
+ differential: float
42
+
43
+
44
+ def _load_points(path: str) -> pd.DataFrame:
45
+ """Flatten one report JSON into a tidy long DataFrame (one row per level×side).
46
+
47
+ Parses the file through `ConcurrencyScalingReport`, so a malformed or truncated report raises a
48
+ clean `ValueError` (the CLI maps it to a friendly error) rather than a raw KeyError, and missing
49
+ optional fields fall back to their model defaults.
50
+ """
51
+ report = _load_report(path)
52
+ rows: list[_PlotRow] = []
53
+ for point in report.points:
54
+ if point.world_wall_mean:
55
+ rows.append(
56
+ _PlotRow(
57
+ level=point.level,
58
+ side="world model",
59
+ wall=point.world_wall_mean,
60
+ wall_std=point.world_wall_std,
61
+ speedup=point.speedup,
62
+ efficiency=point.efficiency,
63
+ differential=point.differential,
64
+ )
65
+ )
66
+ if point.real_wall_mean:
67
+ rows.append(
68
+ _PlotRow(
69
+ level=point.level,
70
+ side="real sandbox",
71
+ wall=point.real_wall_mean,
72
+ wall_std=point.real_wall_std,
73
+ speedup=0.0,
74
+ efficiency=0.0,
75
+ differential=point.differential,
76
+ )
77
+ )
78
+ if not rows:
79
+ raise ValueError(f"no timed points in {path}")
80
+ return pd.DataFrame([r.model_dump() for r in rows])
81
+
82
+
83
+ # Brand system (AGENTS.md rule 15): white bg, near-black ink, hairline grid, brand-palette accents.
84
+ # Ref: scripts/plot_trace_scaling.py.
85
+ _INK = "#0a0a0a"
86
+ _MUTED = "#8a8a8a"
87
+ _GRID = "#ececec"
88
+ _WORLD_COLOR = "#0070f3" # world model — primary blue
89
+ _REAL_COLOR = "#7928ca" # real sandbox — purple
90
+ _DIFF_COLOR = "#e00" # differential — red
91
+ _IDEAL_COLOR = "#8a8a8a" # ideal / parity reference — muted grey
92
+ # Region tints for the differential panel — light washes of the brand teal (positive) and red.
93
+ _FASTER_FILL = "#e8faf6" # region where the world model is faster (light teal, tint of #50e3c2)
94
+ _SLOWER_FILL = "#fdeaea" # region where the real sandbox is faster (light red, tint of #ee0000)
95
+ # Distinct per-benchmark accents for the cross-benchmark combined figure — brand palette, in order.
96
+ _BENCH_COLORS = ("#0070f3", "#f5a623", "#ee0000", "#7928ca", "#50e3c2")
97
+
98
+
99
+ def _bench_color(index: int) -> str:
100
+ """Stable brand accent for benchmark `index` in the combined overlay."""
101
+ return _BENCH_COLORS[index % len(_BENCH_COLORS)]
102
+
103
+
104
+ def _fmt_secs(seconds: float) -> str:
105
+ """Compact seconds label for point annotations (e.g. 3.6s, 27.6s, 1224s)."""
106
+ return f"{seconds:.0f}s" if seconds >= 100 else f"{seconds:.1f}s"
107
+
108
+
109
+ def _load_report(path: str) -> ConcurrencyScalingReport:
110
+ """Parse a report JSON, mapping a schema failure to a clean ValueError the CLI can surface."""
111
+ text = Path(path).read_text(encoding="utf-8")
112
+ try:
113
+ return ConcurrencyScalingReport.model_validate_json(text)
114
+ except ValidationError as exc:
115
+ raise ValueError(f"{path} is not a valid concurrency-scaling report: {exc}") from exc
116
+
117
+
118
+ def _styled_line(ax: plt.Axes, xs: list[int], ys: list[float], *, color: str, label: str) -> None:
119
+ """Plot one series in the shared brand marker/line style (used by every panel and figure)."""
120
+ ax.plot(
121
+ xs,
122
+ ys,
123
+ "-o",
124
+ color=color,
125
+ label=label,
126
+ linewidth=2.2,
127
+ markersize=6,
128
+ markerfacecolor="white",
129
+ markeredgecolor=color,
130
+ markeredgewidth=1.6,
131
+ zorder=3,
132
+ )
133
+
134
+
135
+ def _style_panel(ax: plt.Axes, levels: list[int], *, title: str, xlabel: str, ylabel: str) -> None:
136
+ """Apply the shared brand chrome to a panel: hairline grid, no top/right spine, muted ticks."""
137
+ ax.set_title(title, fontsize=13, color=_INK, fontweight="bold", loc="left", pad=12)
138
+ ax.set_xlabel(xlabel, fontsize=11, color=_INK)
139
+ ax.set_ylabel(ylabel, fontsize=11, color=_INK)
140
+ for side in ("top", "right"):
141
+ ax.spines[side].set_visible(False)
142
+ for side in ("left", "bottom"):
143
+ ax.spines[side].set_color(_GRID)
144
+ ax.grid(axis="y", color=_GRID, linewidth=1)
145
+ ax.set_axisbelow(True)
146
+ ax.tick_params(colors=_MUTED, labelsize=10, length=0)
147
+ _style_level_axis(ax, levels)
148
+ leg = ax.get_legend()
149
+ if leg is not None:
150
+ leg.set_frame_on(False)
151
+ for text in leg.get_texts():
152
+ text.set_color(_INK)
153
+
154
+
155
+ def _style_level_axis(ax: plt.Axes, levels: list[int]) -> None:
156
+ """Log2 x-axis with plain integer concurrency ticks (1, 2, 4, ...), shared by every panel."""
157
+ ax.set_xscale("log", base=2)
158
+ ax.set_xticks(levels)
159
+ ax.get_xaxis().set_major_formatter(mticker.ScalarFormatter())
160
+ ax.set_xlim(levels[0] * 0.85, levels[-1] * 1.18)
161
+ ax.margins(y=0.12)
162
+
163
+
164
+ def render_report(path: str, out: str, *, title: str = "Concurrency scaling law") -> str:
165
+ """Render the report JSON at `path` to an image at `out`; return `out`.
166
+
167
+ Three panels, all comparing the SAME two sides so the world-model-vs-real-sandbox story threads
168
+ through every one:
169
+ 1. batch wall-clock per side (absolute time, log y, endpoint seconds labelled),
170
+ 2. the headline time differential T_real/T_world (how many times faster, with the parity
171
+ crossing shaded green=world-faster / red=real-faster),
172
+ 3. how each side parallelizes (speed-up vs W=1 against ideal-linear, log-log).
173
+ Fixed benchmark-agnostic styling so all benchmarks line up. When only the world side was timed
174
+ (`--side world`), the real-sandbox series are simply absent and panel 2 says so.
175
+ """
176
+ df = _load_points(path)
177
+ has_real = bool((df["side"] == "real sandbox").any())
178
+ has_diff = bool((df["differential"] > 0).any())
179
+ levels = sorted(df["level"].unique())
180
+ fig, axes = plt.subplots(1, 3, figsize=(19, 5.5), dpi=200)
181
+ fig.patch.set_facecolor("white")
182
+
183
+ def line(ax: plt.Axes, xs: list[int], ys: list[float], color: str, label: str) -> None:
184
+ _styled_line(ax, xs, ys, color=color, label=label)
185
+
186
+ # 1) Batch wall-clock vs. concurrency (log y), one line per side, mean±std band, endpoint labels
187
+ # so the absolute seconds (and the gap between the two sides) read at a glance.
188
+ ax = axes[0]
189
+ ax.set_facecolor("white")
190
+ for side, color in (("world model", _WORLD_COLOR), ("real sandbox", _REAL_COLOR)):
191
+ grp = df[df["side"] == side].sort_values("level")
192
+ if grp.empty:
193
+ continue
194
+ xs, ys = list(grp["level"]), list(grp["wall"])
195
+ line(ax, xs, ys, color, side)
196
+ lo = [w - s for w, s in zip(grp["wall"], grp["wall_std"], strict=True)]
197
+ hi = [w + s for w, s in zip(grp["wall"], grp["wall_std"], strict=True)]
198
+ ax.fill_between(grp["level"], lo, hi, color=color, alpha=0.12, linewidth=0, zorder=2)
199
+ for x, y in ((xs[0], ys[0]), (xs[-1], ys[-1])): # label the W=1 and W=max endpoints
200
+ ax.annotate(
201
+ _fmt_secs(y),
202
+ (x, y),
203
+ textcoords="offset points",
204
+ xytext=(0, 10),
205
+ ha="center",
206
+ fontsize=8.5,
207
+ color=color,
208
+ fontweight="bold",
209
+ )
210
+ ax.set_yscale("log")
211
+ ax.legend(loc="best", fontsize=10)
212
+ _style_panel(
213
+ ax,
214
+ levels,
215
+ title="Wall-clock to obtain the batch (log scale, lower = faster)",
216
+ xlabel="concurrency (scenarios at once)",
217
+ ylabel="batch wall-clock (s)",
218
+ )
219
+
220
+ # 2) The headline: time differential T_real / T_world — how many times faster the world model.
221
+ # Shade the world-faster (>1) and real-faster (<1) regions so the parity crossing reads at a
222
+ # glance (tau dips below 1 at high W; terminal/swe stay above). Log y when the range is wide.
223
+ ax = axes[1]
224
+ ax.set_facecolor("white")
225
+ diff = df[(df["side"] == "real sandbox") & (df["differential"] > 0)].sort_values("level")
226
+ if has_diff and not diff.empty:
227
+ dl, dvals = list(diff["level"]), list(diff["differential"])
228
+ line(ax, dl, dvals, _DIFF_COLOR, "T_real / T_world")
229
+ for lvl, d in zip(dl, dvals, strict=True):
230
+ ax.annotate(
231
+ f"{d:.2f}×",
232
+ (lvl, d),
233
+ textcoords="offset points",
234
+ xytext=(6, 6),
235
+ fontsize=9,
236
+ color=_INK,
237
+ fontweight="bold",
238
+ )
239
+ # Log y only when the curve's OWN dynamic range is wide (max/min >= 8) — keying on absolute
240
+ # magnitude would log a high-but-flat curve (e.g. swe ~70-138×, ~2× spread) and cram it into
241
+ # the top of the panel with empty decades below. These curves read best on a linear axis.
242
+ if max(dvals) / min(dvals) >= 8:
243
+ ax.set_yscale("log")
244
+ ax.yaxis.set_major_formatter(mticker.ScalarFormatter())
245
+ lo = min(0.9, min(dvals) * 0.85)
246
+ hi = max(dvals) * 1.25
247
+ ax.set_ylim(lo, hi)
248
+ ax.axhspan(1.0, hi, color=_FASTER_FILL, zorder=0) # world model faster
249
+ if lo < 1.0:
250
+ ax.axhspan(lo, 1.0, color=_SLOWER_FILL, zorder=0) # real sandbox faster
251
+ ax.axhline(1.0, linestyle=(0, (4, 3)), color=_IDEAL_COLOR, linewidth=1.6, label="parity")
252
+ ax.legend(loc="best", fontsize=10)
253
+ else:
254
+ msg = (
255
+ "world-model side only\n(run `--side both` for\nthe sandbox differential)"
256
+ if not has_real
257
+ else "no real-sandbox timings"
258
+ )
259
+ ax.text(0.5, 0.5, msg, ha="center", va="center", transform=ax.transAxes, color=_MUTED)
260
+ _style_panel(
261
+ ax,
262
+ levels,
263
+ title="World model speed-up over the real sandbox",
264
+ xlabel="concurrency",
265
+ ylabel="T_real / T_world (>1 = world model faster)",
266
+ )
267
+
268
+ # 3) Mechanism: how each side parallelizes — speed-up vs W=1 against the ideal-linear diagonal,
269
+ # log-log so ideal is a straight 45° reference and the sub-linear curves stay legible.
270
+ ax = axes[2]
271
+ ax.set_facecolor("white")
272
+ ax.plot(
273
+ levels,
274
+ levels,
275
+ linestyle=(0, (4, 3)),
276
+ color=_IDEAL_COLOR,
277
+ linewidth=1.6,
278
+ label="ideal (linear)",
279
+ zorder=1,
280
+ )
281
+ baseline_level = levels[0]
282
+ for side, color in (("world model", _WORLD_COLOR), ("real sandbox", _REAL_COLOR)):
283
+ grp = df[df["side"] == side].sort_values("level")
284
+ base_rows = grp[grp["level"] == baseline_level]
285
+ if grp.empty or base_rows.empty:
286
+ # No timing at the baseline level -> can't normalize this side to the same W as the
287
+ # other, so skip it rather than plot a curve on a different (higher) baseline.
288
+ continue
289
+ base = float(base_rows.iloc[0]["wall"])
290
+ speedup = [base / w if w else 0.0 for w in grp["wall"]]
291
+ line(ax, list(grp["level"]), speedup, color, side)
292
+ ax.set_yscale("log", base=2)
293
+ ax.yaxis.set_major_formatter(mticker.ScalarFormatter())
294
+ ax.legend(loc="upper left", fontsize=10)
295
+ _style_panel(
296
+ ax,
297
+ levels,
298
+ title="How each side parallelizes (speed-up vs W=1)",
299
+ xlabel="concurrency",
300
+ ylabel="speed-up vs. W=1 (log)",
301
+ )
302
+
303
+ fig.suptitle(title, fontsize=15, color=_INK, fontweight="bold", x=0.02, ha="left")
304
+ fig.tight_layout()
305
+ fig.savefig(out, bbox_inches="tight", facecolor="white")
306
+ plt.close(fig)
307
+ return out
308
+
309
+
310
+ def _load_reports(paths: list[str]) -> tuple[list[tuple[str, ConcurrencyScalingReport]], list[int]]:
311
+ """Load each report JSON (labelled by its `benchmark` field, else file stem) + shared levels.
312
+
313
+ Raises a clean `ValueError` (surfaced by the CLI as a friendly error) when the list is empty or
314
+ no report has a single timed point.
315
+ """
316
+ reports: list[tuple[str, ConcurrencyScalingReport]] = []
317
+ for path in paths:
318
+ report = _load_report(path)
319
+ reports.append((report.benchmark or Path(path).stem, report))
320
+ if not reports:
321
+ raise ValueError("no reports to plot")
322
+ levels = sorted({p.level for _, r in reports for p in r.points})
323
+ if not levels:
324
+ raise ValueError("reports have no timed points to plot")
325
+ return reports, levels
326
+
327
+
328
+ def _header(fig: plt.Figure, title: str) -> None:
329
+ """One clean left-aligned title at the top of a single-panel figure (Vercel/Notion minimal).
330
+
331
+ No subtitle or caption: the title carries the message, the chart carries the rest.
332
+ `subplots_adjust` reserves a generous top band so the title breathes above the axes.
333
+ """
334
+ fig.text(0.02, 0.955, title, fontsize=14.5, fontweight="bold", color=_INK, ha="left", va="top")
335
+ fig.subplots_adjust(top=0.83, left=0.1, right=0.93, bottom=0.13)
336
+
337
+
338
+ def render_speedup(
339
+ paths: list[str],
340
+ out: str,
341
+ *,
342
+ title: str = "How much faster is the world model than the real environment?",
343
+ ) -> str:
344
+ """Render the cross-benchmark speed-up figure (T_real/T_world per benchmark) to `out`.
345
+
346
+ One line per benchmark on a log y-axis: how many times faster the world model is than standing
347
+ up the real environment, at each concurrency level. The region above the parity line is tinted
348
+ (world model faster); a line dipping below it (e.g. tau-bench's cheap in-process env) is where
349
+ the real sandbox wins. Lines are directly labelled at their right end, so the figure needs no
350
+ legend lookup. This is the "what"; `render_cost` is the "why".
351
+ """
352
+ reports, levels = _load_reports(paths)
353
+ fig, ax = plt.subplots(figsize=(8.6, 5.6), dpi=200)
354
+ fig.patch.set_facecolor("white")
355
+ ax.set_facecolor("white")
356
+
357
+ all_diffs: list[float] = []
358
+ for idx, (label, report) in enumerate(reports):
359
+ pts = [
360
+ (p.level, p.differential)
361
+ for p in report.points
362
+ if p.differential and p.differential > 0
363
+ ]
364
+ if not pts:
365
+ continue
366
+ xs = [lvl for lvl, _ in pts]
367
+ ys = [v for _, v in pts]
368
+ all_diffs.extend(ys)
369
+ color = _bench_color(idx)
370
+ _styled_line(ax, xs, ys, color=color, label=label)
371
+ for x, y in ((xs[0], ys[0]), (xs[-1], ys[-1])): # value-label both endpoints
372
+ ax.annotate(
373
+ f"{y:.1f}×",
374
+ (x, y),
375
+ textcoords="offset points",
376
+ xytext=(0, 9),
377
+ ha="center",
378
+ fontsize=8.5,
379
+ color=color,
380
+ fontweight="bold",
381
+ )
382
+ # Direct-label the line at its right end (replaces a legend the reader has to cross-check).
383
+ ax.annotate(
384
+ label,
385
+ (xs[-1], ys[-1]),
386
+ textcoords="offset points",
387
+ xytext=(12, -1),
388
+ ha="left",
389
+ va="center",
390
+ fontsize=10,
391
+ color=color,
392
+ fontweight="bold",
393
+ annotation_clip=False,
394
+ )
395
+
396
+ ax.set_yscale("log")
397
+ ax.yaxis.set_major_formatter(mticker.ScalarFormatter())
398
+ if all_diffs:
399
+ hi = max(all_diffs) * 1.7
400
+ lo = min(0.9, min(all_diffs) * 0.82)
401
+ ax.set_ylim(lo, hi)
402
+ # Colour alone carries the meaning (no labels): teal above parity = world model faster,
403
+ # red below = real environment faster.
404
+ ax.axhspan(1.0, hi, color=_FASTER_FILL, zorder=0)
405
+ if lo < 1.0:
406
+ ax.axhspan(lo, 1.0, color=_SLOWER_FILL, zorder=0)
407
+ ax.axhline(1.0, linestyle=(0, (4, 3)), color=_IDEAL_COLOR, linewidth=1.4)
408
+ ax.annotate(
409
+ "parity (1×)",
410
+ (levels[-1], 1.0),
411
+ textcoords="offset points",
412
+ xytext=(0, 6),
413
+ ha="right",
414
+ fontsize=8.5,
415
+ color=_MUTED,
416
+ )
417
+ _style_panel(
418
+ ax,
419
+ levels,
420
+ title="",
421
+ xlabel="concurrency (scenarios reconstructed at once)",
422
+ ylabel="times faster (T_real / T_world)",
423
+ )
424
+ ax.set_xlim(levels[0] * 0.85, levels[-1] * 1.75) # room for the right-end benchmark labels
425
+ _header(fig, title)
426
+ fig.savefig(out, bbox_inches="tight", facecolor="white")
427
+ plt.close(fig)
428
+ return out
429
+
430
+
431
+ def render_cost(
432
+ paths: list[str],
433
+ out: str,
434
+ *,
435
+ title: str = "Why: reconstructing vs. standing up the real environment",
436
+ ) -> str:
437
+ """Render the cross-benchmark cost figure (world vs. real wall-clock at W=1) to `out`.
438
+
439
+ Grouped log-y bars per benchmark: the world-model reconstruction cost (blue) beside the real
440
+ environment's standup cost (purple), at the lowest concurrency level. Where the purple bar
441
+ towers over the blue one the world model wins; where it is shorter it loses. The per-benchmark
442
+ speed-up (×) is annotated above each pair to tie this mechanism back to `render_speedup`.
443
+ """
444
+ reports, levels = _load_reports(paths)
445
+ base_level = levels[0]
446
+ names: list[str] = []
447
+ w_costs: list[float] = []
448
+ r_costs: list[float] = []
449
+ for label, report in reports:
450
+ pt = next((p for p in report.points if p.level == base_level), None)
451
+ if pt is None:
452
+ continue
453
+ names.append(label)
454
+ w_costs.append(pt.world_wall_mean or 0.0)
455
+ r_costs.append(pt.real_wall_mean or 0.0)
456
+ if not names:
457
+ raise ValueError(f"no report has a point at the baseline concurrency W={base_level}")
458
+
459
+ fig, ax = plt.subplots(figsize=(8.6, 5.6), dpi=200)
460
+ fig.patch.set_facecolor("white")
461
+ ax.set_facecolor("white")
462
+ xs = list(range(len(names)))
463
+ width = 0.36
464
+ ax.bar(
465
+ [i - width / 2 for i in xs],
466
+ w_costs,
467
+ width,
468
+ color=_WORLD_COLOR,
469
+ label="world model (reconstruct)",
470
+ zorder=3,
471
+ )
472
+ ax.bar(
473
+ [i + width / 2 for i in xs],
474
+ r_costs,
475
+ width,
476
+ color=_REAL_COLOR,
477
+ label="real environment (stand up)",
478
+ zorder=3,
479
+ )
480
+ ax.set_yscale("log")
481
+
482
+ def _bar_label(x: float, value: float, color: str) -> None:
483
+ if value <= 0:
484
+ return
485
+ ax.annotate(
486
+ _fmt_secs(value),
487
+ (x, value),
488
+ textcoords="offset points",
489
+ xytext=(0, 3),
490
+ ha="center",
491
+ fontsize=8.5,
492
+ color=color,
493
+ fontweight="bold",
494
+ )
495
+
496
+ top = max([*w_costs, *r_costs, 1.0])
497
+ for i, (w, r) in enumerate(zip(w_costs, r_costs, strict=True)):
498
+ _bar_label(i - width / 2, w, _WORLD_COLOR)
499
+ _bar_label(i + width / 2, r, _REAL_COLOR)
500
+ if w > 0 and r > 0: # speed-up (×) just above the pair's taller bar, tying back to fig 1
501
+ ax.annotate(
502
+ f"{r / w:.1f}× faster" if r >= w else f"{r / w:.2f}× (slower)",
503
+ (i, max(w, r) * 1.6),
504
+ ha="center",
505
+ fontsize=9.5,
506
+ color=_INK,
507
+ fontweight="bold",
508
+ )
509
+ ax.set_ylim(top=top * 3.2) # headroom so the tallest pair's ×-label clears the top
510
+ _style_panel(ax, levels, title="", xlabel="", ylabel="batch wall-clock at W=1 (s, log)")
511
+ ax.set_xscale("linear") # categorical x (benchmarks), overriding the shared log-level axis
512
+ ax.set_xticks(xs)
513
+ ax.set_xticklabels(names, fontsize=11, color=_INK)
514
+ ax.set_xlim(-0.6, len(names) - 0.4)
515
+ # Upper-left: the empty band above the shortest (leftmost) pair, clear of every ×-label.
516
+ ax.legend(loc="upper left", fontsize=10, frameon=False, labelcolor=_INK)
517
+ _header(fig, title)
518
+ fig.savefig(out, bbox_inches="tight", facecolor="white")
519
+ plt.close(fig)
520
+ return out
521
+
522
+
523
+ __all__ = ["render_cost", "render_report", "render_speedup"]