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/engine/replay.py ADDED
@@ -0,0 +1,443 @@
1
+ """Open-loop reconstruction-fidelity evaluation by replaying held-out steps ("open replay").
2
+
3
+ Replay is TEACHER-FORCED and so perfectly repeatable per step: for each held-out step we feed
4
+ all *real recorded* prior same-trace steps plus the step's `(state_before, action)`, have the world
5
+ model predict the observation, then score it against the *real recorded* observation. Nothing the
6
+ model generates feeds forward, so a bad prediction at one step never contaminates another — the
7
+ score isolates per-step fidelity.
8
+
9
+ The judge is pluggable; `RubricJudge` (the Qwen-AgentWorld-style 5-dimension scorer) is the default
10
+ for evaluation, and the report carries per-step scores plus their mean ± std across steps.
11
+
12
+ Retrieval mirrors serving and GEPA: leak-free demos from the TRAIN corpus only, never the query
13
+ step's own trace.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import random
19
+ from collections.abc import Iterable
20
+ from concurrent.futures import ThreadPoolExecutor
21
+ from statistics import fmean, pstdev
22
+
23
+ from pydantic import BaseModel, Field
24
+
25
+ from wmo.core.render import render_action
26
+ from wmo.core.types import Observation, Step, Trace
27
+ from wmo.engine.grounding import (
28
+ Grounder,
29
+ SourceResolver,
30
+ prefetched_knowledge,
31
+ registry_grounded_knowledge,
32
+ source_grounded_knowledge,
33
+ )
34
+ from wmo.engine.workspace import (
35
+ RepoTreeResolver,
36
+ textop_grounded_knowledge,
37
+ tree_grounded_knowledge,
38
+ )
39
+ from wmo.optimize.gepa import distill_profile, predict_observation, verify_observation
40
+ from wmo.optimize.judge import Judge
41
+ from wmo.providers.base import Provider
42
+ from wmo.retrieval import Retriever
43
+ from wmo.retrieval.leakfree import DemoRetriever
44
+
45
+ # Turns scored per trace when sample_turns="sampled", following Qwen-AgentWorld's protocol:
46
+ # first, last, and 3 uniformly-sampled intermediate turns.
47
+ SAMPLED_TURNS = 5
48
+
49
+
50
+ class StepResult(BaseModel):
51
+ """One replayed step: model prediction vs. recorded truth, plus the judge's verdict.
52
+
53
+ `dimensions` is the per-dimension breakdown when a rubric judge is used (empty otherwise).
54
+ """
55
+
56
+ trace_id: str
57
+ task: str | None = None
58
+ action: str # rendered action, for human-readable scorecards
59
+ actual: str
60
+ predicted: str
61
+ score: float # judge fidelity score, 0..1
62
+ dimensions: dict[str, float] = Field(default_factory=dict) # per-dimension (rubric judge)
63
+ critique: str = ""
64
+ is_error_actual: bool = False
65
+ is_error_predicted: bool = False
66
+ # The model's deliberation in reasoning mode (empty otherwise). Never part of the scored
67
+ # observation — carried so humans can read WHY the env decided success vs. error.
68
+ reasoning: str = ""
69
+ valid: bool = True # False = the judge failed on this step; excluded from fidelity aggregates
70
+ # The model's stated confidence in confidence mode (None when absent/off) plus its optional
71
+ # one-line justification. Analysis-only, same rule as `reasoning`: the judge never sees it.
72
+ confidence: float | None = None
73
+ confidence_why: str = ""
74
+ # Whether the verify second pass actually ran on this step — always-verify sets it on every
75
+ # step; confidence-gated verify (`verify_below`) only where the draft's confidence fell
76
+ # under the threshold. This is the population count every gated-cost claim divides by.
77
+ verified: bool = False
78
+ # Whether the step was re-predicted on the escalation provider (confidence-gated model
79
+ # escalation, `escalate_below`). Same accounting role as `verified`.
80
+ escalated: bool = False
81
+
82
+
83
+ class ReplayReport(BaseModel):
84
+ """Aggregate fidelity over a replay run."""
85
+
86
+ mean_score: float = 0.0
87
+ score_std: float = 0.0 # spread of per-step scores across steps (uniform vs uneven fidelity)
88
+ error_flag_accuracy: float = 0.0 # fraction where predicted is_error matched actual
89
+ n_steps: int = 0
90
+ n_invalid: int = 0 # steps where the judge failed; kept in `results` but not in the mean/std
91
+ results: list[StepResult] = Field(default_factory=list)
92
+
93
+ def summary(self) -> str:
94
+ invalid = f" invalid={self.n_invalid}" if self.n_invalid else ""
95
+ return (
96
+ f"fidelity={self.mean_score:.3f}±{self.score_std:.3f} "
97
+ f"error_flag_acc={self.error_flag_accuracy:.3f} n={self.n_steps}{invalid}"
98
+ )
99
+
100
+
101
+ def replay(
102
+ prompt: str,
103
+ held_out: list[Trace],
104
+ provider: Provider,
105
+ judge: Judge,
106
+ *,
107
+ retriever: Retriever | None = None,
108
+ train: list[Trace] | None = None,
109
+ top_k: int = 5,
110
+ sample_turns: str = "all",
111
+ seed: int = 0,
112
+ concurrency: int = 1,
113
+ knowledge: str | None = None,
114
+ reasoning: bool = False,
115
+ grounder: Grounder | None = None,
116
+ verify: bool = False,
117
+ source: SourceResolver | None = None,
118
+ source_annotate_stale: bool = False,
119
+ tree: RepoTreeResolver | None = None,
120
+ profile: bool = False,
121
+ poll: bool = False,
122
+ confidence: bool = False,
123
+ confidence_why: bool = False,
124
+ verify_below: float | None = None,
125
+ escalate_provider: Provider | None = None,
126
+ escalate_below: float | None = None,
127
+ max_retrieved_observation_chars: int | None = None,
128
+ ) -> ReplayReport:
129
+ """Replay held-out steps, scoring predicted vs. actual observations.
130
+
131
+ - `sample_turns`: "all" scores every step; "sampled" scores first/last/3-uniform per trace
132
+ (Qwen-AgentWorld's 5-turn protocol) using `seed` for reproducible turn selection.
133
+ - `retriever` + `train` enable leak-free RAG (demos from the train corpus, never the own trace);
134
+ omit either for zero-shot.
135
+ - `knowledge`/`reasoning`: the serving engine's agentic mode (rendered knowledge-base text +
136
+ the deliberate-then-answer contract). Callers own leak-freedom: `knowledge` must be derived
137
+ from TRAIN traces only (see `wmo.engine.knowledge.seed_knowledge`).
138
+ - `grounder`: prefetch a step's read-only `curl` GET URL live and put the real body in
139
+ context (mirrors the serving engine's prefetch). NON-HERMETIC — the web has moved since
140
+ capture — so it stays off everywhere except explicitly-labeled experiments.
141
+ - `verify`: a second self-check completion per step (draft re-examined against the evidence,
142
+ the revision is what gets scored). Doubles the per-step provider cost.
143
+ - `source`: ground FIRST-TOUCH file-read actions in the real pinned repo file (see
144
+ `SourceResolver`). Requires traces pinned by `instance_id`; steps on unpinned traces and
145
+ previously-touched paths are untouched.
146
+ - `profile`: one digest completion per step revises the teacher-forced history into a
147
+ current-state belief profile ("what is running NOW") injected alongside the knowledge —
148
+ the eval face of the serve-side `state_update` revision. Extra completion per step with
149
+ history.
150
+ - `confidence`/`confidence_why`: the verbalized-confidence contract fields (WS-A6, D75); the
151
+ stated value lands on `StepResult.confidence`, never in the judged observation.
152
+ - `verify_below`: confidence-GATED verify — the second self-check completion runs only when
153
+ the draft states confidence < `verify_below` (a missing confidence counts as low). Raises
154
+ unless `confidence=True` (a gate with no stated confidence would silently always fire);
155
+ independent of `verify` (which remains always-verify).
156
+ - `escalate_provider`/`escalate_below`: confidence-gated MODEL escalation — when the draft
157
+ (from `provider`, the cheap model) states confidence < `escalate_below`, the step is
158
+ re-predicted from scratch on `escalate_provider` (the strong model) and that prediction is
159
+ what gets scored (and verify-gated). Fresh re-prediction, not a revision: anchoring the
160
+ strong model on a weak draft is the failure mode this avoids.
161
+ - `poll`: two zero-completion grounding channels — live registry polls (PyPI/npm JSON for
162
+ `pip show/install` / `npm view` actions; NON-HERMETIC like `grounder`) and deterministic
163
+ text-op answers (wc/sort/uniq computed in pure Python over content the session itself
164
+ wrote; hermetic, refuses when the bytes aren't fully known).
165
+ - `concurrency`: steps are independent (each a predict + judge round trip), so `concurrency > 1`
166
+ scores them on a thread pool — the result is identical and order-preserving (only the wall
167
+ clock changes). Default 1 keeps existing callers unchanged; raise it to cut latency on large
168
+ held-out sets when the provider quota allows.
169
+
170
+ Each step is scored once (the world model is queried deterministically). `score_std` is the
171
+ spread of per-step scores *across steps*, not across repeated samples — sampling the world model
172
+ multiple times per step needs temperature support in the provider layer (no backend forwards it
173
+ today; tracked with the GEPA temperature work).
174
+ """
175
+ if (verify_below is not None or escalate_below is not None) and not confidence:
176
+ raise ValueError(
177
+ "verify_below/escalate_below gate on the STATED confidence — pass confidence=True so"
178
+ " the contract asks for one. Without it every draft parses to no-confidence, the"
179
+ " gate fires on 100% of steps, and a 'gated' run silently pays the always-on bill."
180
+ )
181
+ if (escalate_provider is None) != (escalate_below is None):
182
+ raise ValueError(
183
+ "escalate_provider and escalate_below must be set together: the provider without a"
184
+ " threshold (or vice versa) would be a silent no-op."
185
+ )
186
+ demos = DemoRetriever(retriever, train or [], top_k=top_k)
187
+ rng = random.Random(seed)
188
+ # Materialize the (step, history) work list first — selection uses `rng` and must stay
189
+ # sequential/deterministic; scoring each item is independent and order is restored below.
190
+ work: list[tuple[str, str | None, Step, list[Step]]] = []
191
+ for trace in held_out:
192
+ instance = trace.metadata.get("instance_id")
193
+ instance_id = instance if isinstance(instance, str) else None
194
+ for step_index in _select_step_indices(trace, sample_turns, rng):
195
+ work.append(
196
+ (trace.trace_id, instance_id, trace.steps[step_index], trace.steps[:step_index])
197
+ )
198
+
199
+ def _score(item: tuple[str, str | None, Step, list[Step]]) -> StepResult:
200
+ trace_id, instance_id, step, history = item
201
+ return _score_step(
202
+ prompt,
203
+ trace_id,
204
+ step,
205
+ provider,
206
+ judge,
207
+ demos,
208
+ history,
209
+ knowledge=knowledge,
210
+ reasoning=reasoning,
211
+ grounder=grounder,
212
+ verify=verify,
213
+ source=source,
214
+ source_annotate_stale=source_annotate_stale,
215
+ tree=tree,
216
+ instance_id=instance_id,
217
+ profile=profile,
218
+ poll=poll,
219
+ confidence=confidence,
220
+ confidence_why=confidence_why,
221
+ verify_below=verify_below,
222
+ escalate_provider=escalate_provider,
223
+ escalate_below=escalate_below,
224
+ max_retrieved_observation_chars=max_retrieved_observation_chars,
225
+ )
226
+
227
+ if concurrency > 1 and len(work) > 1:
228
+ with ThreadPoolExecutor(max_workers=concurrency) as pool:
229
+ results = list(pool.map(_score, work)) # map preserves input order
230
+ else:
231
+ results = [_score(item) for item in work]
232
+ return _aggregate(results)
233
+
234
+
235
+ def _select_step_indices(trace: Trace, sample_turns: str, rng: random.Random) -> list[int]:
236
+ """Pick which steps of `trace` to score. 'all' = every step; 'sampled' = first/last/3 middle."""
237
+ steps = trace.steps
238
+ if sample_turns != "sampled" or len(steps) <= SAMPLED_TURNS:
239
+ return list(range(len(steps)))
240
+ middle = list(range(1, len(steps) - 1))
241
+ picks = sorted(rng.sample(middle, SAMPLED_TURNS - 2))
242
+ return [0, *picks, len(steps) - 1]
243
+
244
+
245
+ def _score_step(
246
+ prompt: str,
247
+ trace_id: str,
248
+ step: Step,
249
+ provider: Provider,
250
+ judge: Judge,
251
+ demos: DemoRetriever,
252
+ history: list[Step],
253
+ *,
254
+ knowledge: str | None = None,
255
+ reasoning: bool = False,
256
+ grounder: Grounder | None = None,
257
+ verify: bool = False,
258
+ source: SourceResolver | None = None,
259
+ source_annotate_stale: bool = False,
260
+ tree: RepoTreeResolver | None = None,
261
+ instance_id: str | None = None,
262
+ profile: bool = False,
263
+ poll: bool = False,
264
+ confidence: bool = False,
265
+ confidence_why: bool = False,
266
+ verify_below: float | None = None,
267
+ escalate_provider: Provider | None = None,
268
+ escalate_below: float | None = None,
269
+ max_retrieved_observation_chars: int | None = None,
270
+ ) -> StepResult:
271
+ """Predict the observation for one step and score it against the recorded observation.
272
+
273
+ Two failure modes are handled DIFFERENTLY on purpose:
274
+
275
+ - A *draft prediction* failure (the target times out / throttles / errors and never produces an
276
+ observation) is a genuine fidelity failure: it scores 0.0 with `valid=True` (counted in the
277
+ mean) rather than aborting the whole run - one stalled target request must not throw away
278
+ every other step. ONLY the draft `_predict(provider)` is guarded.
279
+ - *Escalation* and *verify* failures are NOT caught: a systematic failure there (a broken
280
+ escalate_provider or verify pass) aborts loudly rather than being laundered into counted 0.0s
281
+ that silently depress the fidelity mean.
282
+ - A *judge* failure is NOT caught here. A malformed reply already comes back as `valid=False`
283
+ (excluded from aggregates); a judge call that RAISES (throttle/5xx after its own fallover)
284
+ propagates and aborts the eval on purpose - a partially judged run would silently change what
285
+ the fidelity mean is over. The grid guards against this with the judge's same-model fallover.
286
+ """
287
+ step_demos = demos.demos_for(trace_id, step)
288
+ step_knowledge = prefetched_knowledge(knowledge, step.action, grounder)
289
+ prior_actions = [h.action for h in history]
290
+ step_knowledge = source_grounded_knowledge(
291
+ step_knowledge,
292
+ step.action,
293
+ instance_id,
294
+ prior_actions,
295
+ source,
296
+ annotate_stale=source_annotate_stale,
297
+ )
298
+ step_knowledge = tree_grounded_knowledge(
299
+ step_knowledge, step.action, instance_id, prior_actions, tree, source
300
+ )
301
+ if poll:
302
+ step_knowledge = registry_grounded_knowledge(step_knowledge, step.action)
303
+ step_knowledge = textop_grounded_knowledge(step_knowledge, step.action, prior_actions)
304
+ if profile:
305
+ profile_text = distill_profile(provider, step.task, history)
306
+ if profile_text is not None:
307
+ block = f"## environment profile (revised from session history)\n{profile_text}"
308
+ step_knowledge = f"{step_knowledge}\n\n{block}" if step_knowledge else block
309
+
310
+ def _predict(with_provider: Provider) -> Observation:
311
+ return predict_observation(
312
+ with_provider,
313
+ prompt,
314
+ step.task,
315
+ step.state_before,
316
+ step.action,
317
+ demos=step_demos,
318
+ history=history,
319
+ knowledge=step_knowledge,
320
+ reasoning=reasoning,
321
+ confidence=confidence,
322
+ confidence_why=confidence_why,
323
+ max_retrieved_observation_chars=max_retrieved_observation_chars,
324
+ )
325
+
326
+ # Guard ONLY the draft target prediction: a target that times out / throttles / errors and
327
+ # never produces an observation is a real fidelity 0 for this step (valid=True, counted),
328
+ # not a run abort - one stalled request must not throw away every other step. Escalation and
329
+ # verify are deliberately NOT guarded: a systematic failure there (a misconfigured
330
+ # escalate_provider or verify pass) should surface loudly by aborting, not be laundered into a
331
+ # counted 0.0 that silently depresses the fidelity mean. The judge call is also outside (see
332
+ # the docstring).
333
+ try:
334
+ predicted = _predict(provider)
335
+ except Exception as exc: # noqa: BLE001 - a target draft failure is a 0, not a crash
336
+ return StepResult(
337
+ trace_id=trace_id,
338
+ task=step.task,
339
+ action=render_action(step.action),
340
+ actual=step.observation.content,
341
+ predicted="",
342
+ score=0.0,
343
+ critique=f"prediction failed: {type(exc).__name__}: {str(exc)[:200]}",
344
+ is_error_actual=step.observation.is_error,
345
+ is_error_predicted=False,
346
+ )
347
+ escalated = False
348
+ if (
349
+ escalate_provider is not None
350
+ and escalate_below is not None
351
+ and _below(predicted, escalate_below)
352
+ ):
353
+ predicted = _predict(escalate_provider)
354
+ escalated = True
355
+ should_verify = verify or (verify_below is not None and _below(predicted, verify_below))
356
+ if should_verify:
357
+ # The reviser must be the model whose draft is being kept: letting the cheap model revise
358
+ # an escalated (strong-model) prediction would silently undo the escalation on exactly the
359
+ # hard steps it was bought for.
360
+ reviser = escalate_provider if escalated and escalate_provider is not None else provider
361
+ predicted = verify_observation(
362
+ reviser,
363
+ prompt,
364
+ step.task,
365
+ step.state_before,
366
+ step.action,
367
+ predicted,
368
+ demos=step_demos,
369
+ history=history,
370
+ knowledge=step_knowledge,
371
+ reasoning=reasoning,
372
+ confidence=confidence,
373
+ confidence_why=confidence_why,
374
+ max_retrieved_observation_chars=max_retrieved_observation_chars,
375
+ )
376
+ verdict = judge.score(predicted, step.observation, step)
377
+ return StepResult(
378
+ trace_id=trace_id,
379
+ task=step.task,
380
+ action=render_action(step.action),
381
+ actual=step.observation.content,
382
+ predicted=predicted.content,
383
+ score=verdict.score,
384
+ dimensions=verdict.dimensions,
385
+ critique=verdict.critique,
386
+ is_error_actual=step.observation.is_error,
387
+ is_error_predicted=predicted.is_error,
388
+ reasoning=_stated_str(predicted, "reasoning"),
389
+ valid=verdict.valid,
390
+ # The SCORED prediction's stated confidence (after escalation/verify, when they ran) —
391
+ # each gate decided on the confidence stated by the draft it saw.
392
+ confidence=_stated_confidence(predicted),
393
+ confidence_why=_stated_str(predicted, "confidence_why"),
394
+ verified=should_verify,
395
+ escalated=escalated,
396
+ )
397
+
398
+
399
+ def _stated_confidence(observation: Observation) -> float | None:
400
+ """The observation's stated confidence, or None when the model didn't emit one."""
401
+ value = observation.metadata.get("confidence")
402
+ if isinstance(value, int | float) and not isinstance(value, bool):
403
+ return float(value)
404
+ return None
405
+
406
+
407
+ def _below(observation: Observation, threshold: float) -> bool:
408
+ """Gate rule shared by verify_below and escalate_below: missing confidence counts as low."""
409
+ stated = _stated_confidence(observation)
410
+ return stated is None or stated < threshold
411
+
412
+
413
+ def _stated_str(observation: Observation, key: str) -> str:
414
+ """A carried metadata string, degrading to '' when absent or wrong-typed."""
415
+ value = observation.metadata.get(key)
416
+ return value if isinstance(value, str) else ""
417
+
418
+
419
+ def valid_scores(results: Iterable[StepResult]) -> list[float]:
420
+ """Scores of validly-judged steps — the one rule for fidelity aggregation.
421
+
422
+ Judge failures (valid=False) say nothing about the prediction, so every fidelity aggregate
423
+ (here and `wmo.evals.open_loop.evaluate_files`) excludes them rather than counting spurious
424
+ zeros. Kept as the single shared filter so aggregation sites cannot drift.
425
+ """
426
+ return [r.score for r in results if r.valid]
427
+
428
+
429
+ def _aggregate(results: list[StepResult]) -> ReplayReport:
430
+ if not results:
431
+ return ReplayReport()
432
+ # Error-flag accuracy compares recorded flags and is judge-independent, so unlike the
433
+ # fidelity mean/std it stays over every step.
434
+ step_scores = valid_scores(results)
435
+ error_acc = fmean(1.0 if r.is_error_predicted == r.is_error_actual else 0.0 for r in results)
436
+ return ReplayReport(
437
+ mean_score=fmean(step_scores) if step_scores else 0.0,
438
+ score_std=pstdev(step_scores) if len(step_scores) > 1 else 0.0,
439
+ error_flag_accuracy=error_acc,
440
+ n_steps=len(results),
441
+ n_invalid=len(results) - len(step_scores),
442
+ results=results,
443
+ )
@@ -0,0 +1,58 @@
1
+ """Progress reporting for the build pipeline.
2
+
3
+ The build emits coarse lifecycle events (ingest, split, index, optimize, persist) plus fine-grained
4
+ GEPA rollout events to a `BuildReporter`. The engine depends only on this protocol — never on rich —
5
+ so the pipeline stays headless and testable. The CLI supplies a rich-backed reporter
6
+ (`wmo.cli.ui.RichBuildReporter`); everything else uses `NullReporter`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Protocol
12
+
13
+
14
+ class BuildReporter(Protocol):
15
+ """Sink for build-pipeline progress events. All methods are best-effort and side-effecting."""
16
+
17
+ def ingest_done(self, traces: int, steps: int) -> None:
18
+ """Called once after traces are ingested + normalized."""
19
+ ...
20
+
21
+ def split_done(self, train: int, val: int, test: int) -> None:
22
+ """Called once after the train/val/test split (counts are traces)."""
23
+ ...
24
+
25
+ def index_done(self, steps: int) -> None:
26
+ """Called once after the retrieval index is built over the replay buffer."""
27
+ ...
28
+
29
+ def optimize_start(self, budget: int) -> None:
30
+ """Called once before GEPA begins, with the rollout budget."""
31
+ ...
32
+
33
+ def rollout(self, done: int, budget: int, score: float | None) -> None:
34
+ """Called as GEPA consumes its rollout budget. `score` is the mean rollout score so far."""
35
+ ...
36
+
37
+ def activity(self, line: str) -> None:
38
+ """Called with one line of GEPA narration (proposals, selection scores, judge notes)."""
39
+ ...
40
+
41
+ def optimize_done(self, held_out_accuracy: float, frontier_size: int, rollouts: int) -> None:
42
+ """Called once after GEPA finishes."""
43
+ ...
44
+
45
+
46
+ class NullReporter:
47
+ """A reporter that does nothing — the default for library/test callers."""
48
+
49
+ def ingest_done(self, traces: int, steps: int) -> None: ...
50
+ def split_done(self, train: int, val: int, test: int) -> None: ...
51
+ def index_done(self, steps: int) -> None: ...
52
+ def optimize_start(self, budget: int) -> None: ...
53
+ def rollout(self, done: int, budget: int, score: float | None) -> None: ...
54
+ def activity(self, line: str) -> None: ...
55
+
56
+ def optimize_done(
57
+ self, held_out_accuracy: float, frontier_size: int, rollouts: int
58
+ ) -> None: ...