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,291 @@
1
+ """Cross-session knowledge base: the world model's persistent, human-editable memory.
2
+
3
+ A `KnowledgeBase` is a directory of plain markdown files under the model artifact
4
+ (`models/<name>/knowledge/`) holding the environment's *canonical* facts — entities, business
5
+ rules, response schemas, and the state-dependent gates (auth, availability, preconditions) an LLM
6
+ env otherwise guesses wrong. It is:
7
+
8
+ - **seeded at build time** from TRAIN traces only (`seed_knowledge`, an LLM extraction pass),
9
+ - **read at serve time** — rendered whole (size-budgeted) into the env prompt's KNOWLEDGE BASE
10
+ section (`wmo.core.render.build_env_prompt`),
11
+ - **written at serve time** — the env's `kb_note` contract field appends to `learned.md` and the
12
+ grounder caches web results in `grounded.md`; the seeded files are never auto-modified,
13
+ - **edited by humans** — it's just markdown; open the folder in any editor.
14
+
15
+ The session scratchpad (`EnvState.scratchpad`) stays session-local; this store is what persists
16
+ ACROSS sessions. Models without a `knowledge/` directory load and serve exactly as before.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import tempfile
22
+ import threading
23
+ from pathlib import Path
24
+
25
+ from pydantic import BaseModel, ValidationError
26
+
27
+ from wmo.core.parsing import extract_json_object
28
+ from wmo.core.render import render_demo
29
+ from wmo.core.types import Trace
30
+ from wmo.providers.base import Message, Provider
31
+
32
+ # Build-time seeded files (never auto-modified after seeding; humans edit freely).
33
+ SEEDED_FILES = ("rules.md", "entities.md", "schemas.md")
34
+ # Serve-time append targets: env kb_notes and grounder cache. Auto-appended, size-capped.
35
+ LEARNED_FILE = "learned.md"
36
+ GROUNDED_FILE = "grounded.md"
37
+
38
+ # Rendering budget (chars) for the KNOWLEDGE BASE prompt section. The KB is curated facts, not a
39
+ # record dump — tau-bench's full 3.3MB DB per step is exactly the failure mode this cap prevents.
40
+ DEFAULT_RENDER_BUDGET = 24_000
41
+ # Per-file cap (chars) for the auto-appended files, so serve-time writes can't grow unboundedly.
42
+ DEFAULT_APPEND_CAP = 50_000
43
+
44
+ _TRUNCATION_MARKER = "\n[KNOWLEDGE BASE TRUNCATED: over render budget — curate the files]\n"
45
+
46
+
47
+ class KnowledgeBase:
48
+ """A directory of markdown files acting as the env's cross-session memory.
49
+
50
+ Missing directory == empty knowledge base; nothing is created until the first write, so
51
+ models built before this feature (no `knowledge/` dir) are served unchanged.
52
+ """
53
+
54
+ def __init__(self, directory: str | Path, *, append_cap: int = DEFAULT_APPEND_CAP) -> None:
55
+ self.directory = Path(directory)
56
+ self._append_cap = append_cap
57
+ # Serializes read-check-append against itself: FastAPI sync handlers run in a thread
58
+ # pool, so two sessions stepping the SAME served model can race append_learned /
59
+ # append_grounded and double-append (or blow past the cap). In-process only — one
60
+ # server process owns an artifact's knowledge/ dir; cross-process co-serving of one
61
+ # artifact is out of scope.
62
+ self._write_lock = threading.Lock()
63
+
64
+ @property
65
+ def is_empty(self) -> bool:
66
+ """True when no markdown file has any content."""
67
+ return not any(content.strip() for content in self.files().values())
68
+
69
+ def files(self) -> dict[str, str]:
70
+ """Return {file name: content} for every markdown file, in sorted-name order."""
71
+ if not self.directory.is_dir():
72
+ return {}
73
+ with self._write_lock: # a torn read of a file mid-write must not reach the prompt
74
+ return {
75
+ path.name: path.read_text(encoding="utf-8")
76
+ for path in sorted(self.directory.glob("*.md"))
77
+ }
78
+
79
+ def write_file(self, name: str, content: str) -> None:
80
+ """Create/replace one markdown file (seeding, HTTP edit surface)."""
81
+ _validate_file_name(name)
82
+ with self._write_lock: # an HTTP edit racing a stepping session's append
83
+ self.directory.mkdir(parents=True, exist_ok=True)
84
+ (self.directory / name).write_text(content, encoding="utf-8")
85
+
86
+ def render(self, budget: int = DEFAULT_RENDER_BUDGET) -> str:
87
+ """Render the whole KB for the env prompt: `## <file>` sections, sorted, budget-capped.
88
+
89
+ Deterministic: what a human sees in the files is exactly what the env reads. Over-budget
90
+ content is cut with a loud marker rather than silently — an oversized KB is a curation
91
+ problem the user should see, not hidden lossage.
92
+ """
93
+ sections = [
94
+ f"## {name}\n{content.strip()}"
95
+ for name, content in self.files().items()
96
+ if content.strip()
97
+ ]
98
+ text = "\n\n".join(sections)
99
+ if len(text) <= budget:
100
+ return text
101
+ keep = max(budget - len(_TRUNCATION_MARKER), 0)
102
+ return (text[:keep] + _TRUNCATION_MARKER)[:budget]
103
+
104
+ def append_learned(self, fact: str, *, provenance: str) -> bool:
105
+ """Append one cross-session fact (env `kb_note`) to `learned.md` with provenance.
106
+
107
+ Returns False (and writes nothing) when the exact fact is already recorded or the file is
108
+ at its cap. Only `learned.md` is ever auto-written — seeded files and human edits are
109
+ never clobbered.
110
+ """
111
+ fact = fact.strip()
112
+ if not fact:
113
+ return False
114
+ with self._write_lock:
115
+ existing = self._read(LEARNED_FILE)
116
+ # Exact-fact dedupe: a raw substring test treated any new fact that PREFIXES an
117
+ # existing entry as already recorded, silently dropping distinct general facts.
118
+ recorded = {
119
+ line[2:].split(" <!--", 1)[0].strip()
120
+ for line in existing.splitlines()
121
+ if line.startswith("- ")
122
+ }
123
+ if fact in recorded:
124
+ return False
125
+ if len(existing) > self._append_cap:
126
+ return False
127
+ entry = f"- {fact} <!-- {provenance} -->\n"
128
+ self._append(LEARNED_FILE, entry)
129
+ return True
130
+
131
+ def append_grounded(self, query: str, results_text: str) -> None:
132
+ """Cache one grounder result under `grounded.md` so a query is searched at most once."""
133
+ entry = f"### query: {query.strip()}\n{results_text.strip()}\n\n"
134
+ with self._write_lock:
135
+ # Same query grounded by two racing sessions: keep the first cache entry.
136
+ if f"### query: {query.strip()}\n" in self._read(GROUNDED_FILE):
137
+ return
138
+ self._append(GROUNDED_FILE, entry)
139
+
140
+ def lookup_grounded(self, query: str) -> str | None:
141
+ """Return the cached results for `query`, or None on a cache miss."""
142
+ content = self._read(GROUNDED_FILE)
143
+ header = f"### query: {query.strip()}\n"
144
+ start = content.find(header)
145
+ if start == -1:
146
+ return None
147
+ body_start = start + len(header)
148
+ end = content.find("### query: ", body_start)
149
+ return content[body_start : end if end != -1 else len(content)].strip()
150
+
151
+ def _read(self, name: str) -> str:
152
+ path = self.directory / name
153
+ return path.read_text(encoding="utf-8") if path.exists() else ""
154
+
155
+ def _append(self, name: str, text: str) -> None:
156
+ self.directory.mkdir(parents=True, exist_ok=True)
157
+ with (self.directory / name).open("a", encoding="utf-8") as fh:
158
+ fh.write(text)
159
+
160
+
161
+ def _validate_file_name(name: str) -> None:
162
+ if name != Path(name).name or not name.endswith(".md"):
163
+ raise ValueError(
164
+ f"knowledge file name must be a bare '*.md' name (got {name!r}); "
165
+ "the knowledge base is a flat folder of markdown files"
166
+ )
167
+
168
+
169
+ class _Extraction(BaseModel):
170
+ """The seeding LLM's per-chunk output: full updated content of each seeded file."""
171
+
172
+ rules: str = ""
173
+ entities: str = ""
174
+ schemas: str = ""
175
+
176
+
177
+ _SEED_SYSTEM_PROMPT = """You are building the canonical KNOWLEDGE BASE for a simulated \
178
+ environment reconstructed from real agent traces. Extract only DURABLE, CROSS-SESSION facts the \
179
+ environment must stay consistent about:
180
+
181
+ - rules: business rules and STATE-DEPENDENT GATES the environment ITSELF enforces — auth \
182
+ requirements, availability checks, preconditions, completion/cancellation rules. These decide \
183
+ success vs. error. Distinguish carefully: if traces show a tool executing successfully despite \
184
+ a policy the agent was told to follow, that policy is NOT an environment gate — record it as \
185
+ "agent policy (NOT enforced: tools execute mechanically)". Also record SYSTEM LIMITS evidenced \
186
+ in traces with their observed values: command timeouts, rate limits, output truncation, caps.
187
+ - entities: canonical entities that exist (ids, names, relations) — stated generally, never \
188
+ per-session conversation details.
189
+ - schemas: tool/API response shapes, field names, and exact error formats/messages.
190
+
191
+ Do NOT copy session-specific events ("the agent booked X") — only what is true of the \
192
+ environment itself. Prefer terse markdown bullet lists. You are shown the CURRENT knowledge base \
193
+ and a NEW trace excerpt; return the UPDATED full content of all three files (carry existing \
194
+ facts forward, merge and dedupe, drop nothing that is still true).
195
+
196
+ Respond with ONLY a JSON object: {"rules": "<markdown>", "entities": "<markdown>", \
197
+ "schemas": "<markdown>"}"""
198
+
199
+ # Chars of rendered trace text per extraction call. Together with `max_calls` this bounds the
200
+ # build-time seeding cost regardless of corpus size.
201
+ _SEED_CHUNK_CHARS = 40_000
202
+
203
+
204
+ def seed_knowledge(
205
+ kb: KnowledgeBase,
206
+ train_traces: list[Trace],
207
+ provider: Provider,
208
+ *,
209
+ max_calls: int = 8,
210
+ reporter_note: list[str] | None = None,
211
+ ) -> None:
212
+ """Seed `kb` from TRAIN traces via chunked knowledge-accumulation extraction.
213
+
214
+ Renders the corpus steps into text chunks and folds each chunk into the running KB (the same
215
+ accumulate-and-carry-forward pattern GEPA's reflection uses), writing `SEEDED_FILES` after
216
+ every successful call so a partial run still leaves a usable KB. `max_calls` is the hard cost
217
+ bound: chunks beyond it are skipped (coverage saturates quickly — rules/schemas repeat across
218
+ traces). Eval-integrity note: callers must pass TRAIN traces only (mirrors
219
+ `wmo.retrieval.leakfree`), so a KB used during eval can never contain a held-out answer.
220
+ """
221
+ chunks = _corpus_chunks(train_traces)
222
+ skipped = max(len(chunks) - max_calls, 0)
223
+ current = _Extraction(
224
+ rules=kb.files().get("rules.md", ""),
225
+ entities=kb.files().get("entities.md", ""),
226
+ schemas=kb.files().get("schemas.md", ""),
227
+ )
228
+ for chunk in chunks[:max_calls]:
229
+ user = (
230
+ f"CURRENT KNOWLEDGE BASE:\n"
231
+ f"rules.md:\n{current.rules or '(empty)'}\n\n"
232
+ f"entities.md:\n{current.entities or '(empty)'}\n\n"
233
+ f"schemas.md:\n{current.schemas or '(empty)'}\n\n"
234
+ f"NEW TRACE EXCERPT:\n{chunk}"
235
+ )
236
+ completion = provider.complete(_SEED_SYSTEM_PROMPT, [Message(role="user", content=user)])
237
+ extraction = _parse_extraction(completion.text)
238
+ if extraction is None:
239
+ continue # off-contract reply: keep accumulating from the current state
240
+ current = extraction
241
+ kb.write_file("rules.md", current.rules)
242
+ kb.write_file("entities.md", current.entities)
243
+ kb.write_file("schemas.md", current.schemas)
244
+ if skipped and reporter_note is not None:
245
+ reporter_note.append(
246
+ f"knowledge seeding: {skipped} corpus chunk(s) beyond the {max_calls}-call budget "
247
+ "were not read"
248
+ )
249
+
250
+
251
+ def seeded_knowledge_text(
252
+ train_traces: list[Trace], provider: Provider, *, max_calls: int = 8
253
+ ) -> str | None:
254
+ """Seed an ephemeral KB from TRAIN traces and return its rendered text (None when empty).
255
+
256
+ For eval/research contexts that need train-derived knowledge in the prompt without touching
257
+ any model artifact: the KB lives in a temp dir for the duration of seeding only, so nothing a
258
+ serve session ever wrote (learned/grounded facts) can leak into a scored run.
259
+ """
260
+ with tempfile.TemporaryDirectory(prefix="wmo-kb-") as tmp:
261
+ kb = KnowledgeBase(Path(tmp))
262
+ seed_knowledge(kb, train_traces, provider, max_calls=max_calls)
263
+ return kb.render() or None
264
+
265
+
266
+ def _parse_extraction(text: str) -> _Extraction | None:
267
+ raw = extract_json_object(text)
268
+ if raw is None:
269
+ return None
270
+ try:
271
+ return _Extraction.model_validate_json(raw)
272
+ except ValidationError:
273
+ return None
274
+
275
+
276
+ def _corpus_chunks(traces: list[Trace]) -> list[str]:
277
+ """Render every step once (canonical `render_demo`) and pack into char-bounded chunks."""
278
+ chunks: list[str] = []
279
+ buffer: list[str] = []
280
+ size = 0
281
+ for trace in traces:
282
+ for step in trace.steps:
283
+ text = render_demo(step)
284
+ if size + len(text) > _SEED_CHUNK_CHARS and buffer:
285
+ chunks.append("\n\n".join(buffer))
286
+ buffer, size = [], 0
287
+ buffer.append(text)
288
+ size += len(text)
289
+ if buffer:
290
+ chunks.append("\n\n".join(buffer))
291
+ return chunks
wmo/engine/loader.py ADDED
@@ -0,0 +1,36 @@
1
+ """Load a built world model from its artifact directory.
2
+
3
+ One place owns the "artifact dir -> live WorldModel" sequence (read config -> construct the serve
4
+ provider -> `WorldModel.load`). The CLI and the serving layer both call this so the loading path
5
+ stays identical no matter how the model was selected (by name, by picker, or served in bulk).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+
12
+ from wmo.config import load_config
13
+ from wmo.engine.world_model import WorldModel
14
+ from wmo.providers import provider_or_chain
15
+ from wmo.providers.base import Provider
16
+
17
+
18
+ def load_world_model(
19
+ model_dir: str | Path,
20
+ *,
21
+ telemetry_root: str | Path | None = None,
22
+ max_fidelity: bool = False,
23
+ ) -> tuple[WorldModel, Provider]:
24
+ """Load the world model under `model_dir`, returning it with the serve provider it was built on.
25
+
26
+ The provider is returned alongside so callers that also need it (e.g. `wmo demo`, which runs an
27
+ LLM agent against the same provider) don't re-read the config or reconstruct it.
28
+ `max_fidelity` turns on the online extras (the build-measured winner when the artifact has
29
+ one); a plain load runs pure RAG.
30
+ """
31
+ config = load_config(str(model_dir))
32
+ provider = provider_or_chain(config.serve_provider_config())
33
+ wm = WorldModel.load(
34
+ str(model_dir), provider, telemetry_root=telemetry_root, max_fidelity=max_fidelity
35
+ )
36
+ return wm, provider
wmo/engine/play.py ADDED
@@ -0,0 +1,92 @@
1
+ """`wmo play`: a human drives the agent inside the reconstructed environment.
2
+
3
+ This is the interactive sibling of `wmo demo`. Instead of replaying a recorded trajectory,
4
+ a *person* types actions — `tool_name {json args}` or a free-text message — and the world model
5
+ returns the observation, advancing the session (history + scratchpad "database") exactly as a real
6
+ agent would experience it.
7
+
8
+ The engine here is UI-agnostic: it parses a typed line into an `Action` and steps the world model,
9
+ returning the observation alongside the exact env prompt that produced it. The REPL loop, prompts,
10
+ and rendering live in the CLI (`wmo.cli.ui`); this module is what the CLI and its tests call.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+
17
+ from pydantic import BaseModel, JsonValue
18
+
19
+ from wmo.core.parsing import extract_json_object
20
+ from wmo.core.types import Action, ActionKind, JsonObject, Observation
21
+ from wmo.engine.world_model import WorldModel
22
+
23
+
24
+ class PlayTurn(BaseModel):
25
+ """The outcome of one human turn: the action taken, the observation, and the env prompt sent."""
26
+
27
+ action: Action
28
+ observation: Observation
29
+ env_prompt: str
30
+
31
+
32
+ def parse_action(line: str) -> Action:
33
+ """Parse a typed REPL line into an `Action`.
34
+
35
+ Grammar (forgiving, human-first):
36
+ - `get_user {"id": "u1"}` -> tool call `get_user` with JSON arguments
37
+ - `list_flights` -> tool call `list_flights` with no arguments
38
+ - `say hello there` -> free-text message "hello there"
39
+ - anything else -> a free-text message (so the env can react to plain prose)
40
+
41
+ A bare first token is treated as a tool name when it looks like an identifier; otherwise the
42
+ whole line is a message. The `say ` prefix forces a message even when it looks tool-like.
43
+ """
44
+ text = line.strip()
45
+ if not text:
46
+ raise ValueError("empty action")
47
+
48
+ if text.startswith("say "):
49
+ return Action(kind=ActionKind.MESSAGE, content=text[4:].strip())
50
+
51
+ head, _, rest = text.partition(" ")
52
+ rest = rest.strip()
53
+ # A tool call is a bare identifier (`list_flights`) or `name {json args}`. A trailing tail not
54
+ # starting with a JSON bracket means prose (e.g. "what is the weather?") -> treat as a message.
55
+ if _looks_like_tool_name(head) and (not rest or rest.startswith(("{", "["))):
56
+ return Action(kind=ActionKind.TOOL_CALL, name=head, arguments=_parse_arguments(rest))
57
+ return Action(kind=ActionKind.MESSAGE, content=text)
58
+
59
+
60
+ def play_turn(world_model: WorldModel, session_id: str, action: Action) -> PlayTurn:
61
+ """Render the env prompt for `action`, step the world model, and return the full turn."""
62
+ env_prompt = world_model.render_step_prompt(session_id, action)
63
+ observation = world_model.step(session_id, action)
64
+ return PlayTurn(action=action, observation=observation, env_prompt=env_prompt)
65
+
66
+
67
+ def _looks_like_tool_name(token: str) -> bool:
68
+ """A tool name is an ASCII identifier-ish token: [A-Za-z][A-Za-z0-9._-]*.
69
+
70
+ ASCII-only on purpose: `str.isalpha()`/`isalnum()` accept Unicode letters/digits, so without
71
+ this a non-ASCII first word (e.g. "café ..." or "日本 ...") would be misread as a tool call
72
+ instead of a prose message.
73
+ """
74
+ if not token or not ("a" <= token[0].lower() <= "z"):
75
+ return False
76
+ return all(c.isascii() and (c.isalnum() or c in "._-") for c in token)
77
+
78
+
79
+ def _parse_arguments(rest: str) -> JsonObject:
80
+ """Parse the argument tail into a JSON object; empty tail -> no arguments."""
81
+ if not rest:
82
+ return {}
83
+ raw = extract_json_object(rest)
84
+ if raw is None:
85
+ raise ValueError(
86
+ f"could not read tool arguments from {rest!r}; pass a JSON object like "
87
+ '{"id": "u1"} (or omit it for no arguments)'
88
+ )
89
+ parsed: JsonValue = json.loads(raw)
90
+ if not isinstance(parsed, dict):
91
+ raise ValueError('tool arguments must be a JSON object, e.g. {"id": "u1"}')
92
+ return parsed
wmo/engine/prompts.py ADDED
@@ -0,0 +1,99 @@
1
+ """Prompt assembly for the world model.
2
+
3
+ The env prompt is the heart of the system. It composes:
4
+ - the optimized base prompt (layer a / GEPA winner, layer b)
5
+ - the task instruction (tau)
6
+ - the interaction history {(s_i, a_i)}
7
+ - the top-k retrieved demos {d_j}
8
+ - the incoming action
9
+ into the single completion that predicts the next observation (DreamGym Eq. 4).
10
+
11
+ The actual rendering lives in `wmo.core.render` (which depends on nothing), so the serving engine
12
+ and the GEPA optimizer share one assembly — prompts are evolved against exactly what the world model
13
+ serves. This module is the engine-facing entry point: it adapts a live `Session` to that renderer.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from wmo.core.render import build_env_prompt as _build_env_prompt
19
+ from wmo.core.types import Action, Session, Step
20
+
21
+ # Layer (a): the env-agnostic base prompt. GEPA (layer b) evolves a specialized version of this.
22
+ # Tuned via replay-fidelity measurement across example traces:
23
+ # the failure modes a generic prompt makes are (1) fabricating concrete data the env alone knows,
24
+ # (2) inventing stdout when the real command prints nothing, and (3) guessing success/error wrong.
25
+ # This base targets all three while staying domain-agnostic, so it is a strong GEPA starting point.
26
+ BASE_ENV_PROMPT = """You ARE the environment the agent is acting on — a real system (shell, tools,
27
+ database, files), not an assistant. Output ONLY what this environment would actually return for the
28
+ agent's latest action, exactly as the agent would observe it.
29
+
30
+ Infer what KIND of environment this is from the state, history, and examples, and stay in character
31
+ as that system no matter what the agent sends:
32
+ - A shell/terminal responds like a shell. A conversational or malformed action (e.g. the agent types
33
+ "hi" or prose) yields what the shell would emit — e.g. `hi: command not found` and a non-zero
34
+ exit — never a chat reply.
35
+ - An API/tool responds in that API's shape (the same JSON/result schema the examples show), and an
36
+ unrecognized or malformed call yields that API's own error (bad request, unknown endpoint), not an
37
+ explanation.
38
+ - Whatever the surface, react as it would; never break character to talk to the agent.
39
+
40
+ Ground every prediction in the evidence you are given:
41
+ - The environment STATE and INTERACTION HISTORY are the source of truth for concrete values
42
+ (records, ids, prices, file contents, prior effects). Reuse those exact values verbatim.
43
+ - SIMILAR PAST EXAMPLES show how this environment formats responses for analogous actions. Match
44
+ their format, field names, ordering, and error conventions; reuse their values only when the
45
+ current state implies the same ones.
46
+ - When a lookup/read targets something the task or history implies EXISTS (the agent is acting on a
47
+ known id, a referenced record, a file it just created), the environment returns the full populated
48
+ result — so produce a complete, schema-correct, internally-consistent record, not an empty result
49
+ or a "not found" error. Returning "not found" for something that exists is the worst possible
50
+ answer: it flips the outcome. Only return empty/absent when the evidence says it is genuinely
51
+ missing. The fields you can't know (exact prices, dates, ids) should be plausible and mutually
52
+ consistent; the SHAPE and the outcome (found vs. not) are what matter most.
53
+
54
+ Predict precisely:
55
+ - Output exactly the bytes that reach the agent (e.g. stdout/stderr), nothing more. Many commands
56
+ (assignments, writes, redirected output, successful mutations) print NOTHING — return an empty
57
+ observation in that case rather than narrating success.
58
+ - Decide success vs. error from what the action would really do given the state. If it would fail
59
+ (missing record, bad input, syntax error), return the error the environment emits and mark it as
60
+ an error. If it would succeed, do not invent an error.
61
+ - Stay consistent with everything established earlier in the session.
62
+
63
+ Never address the agent, explain your reasoning, or add commentary. Emit only the observation in the
64
+ required output format."""
65
+
66
+
67
+ def build_env_prompt(
68
+ base_prompt: str,
69
+ session: Session,
70
+ action: Action,
71
+ demos: list[Step],
72
+ *,
73
+ knowledge: str | None = None,
74
+ reasoning: bool = False,
75
+ grounding: bool = False,
76
+ confidence: bool = False,
77
+ confidence_why: bool = False,
78
+ max_retrieved_observation_chars: int | None = None,
79
+ ) -> tuple[str, str]:
80
+ """Return (system, user) text for a world-model completion.
81
+
82
+ Mirrors M_exp(R_t | {(s_i,a_i)}, {d_j}, tau): base+task -> system, history+demos+action -> user.
83
+ Delegates to the shared renderer, supplying the session's task, state, and history; the
84
+ agentic-mode flags (`knowledge`/`reasoning`/`grounding`/`confidence`) pass straight through.
85
+ """
86
+ return _build_env_prompt(
87
+ base_prompt,
88
+ session.task,
89
+ session.state,
90
+ action,
91
+ history=session.history,
92
+ demos=demos,
93
+ knowledge=knowledge,
94
+ reasoning=reasoning,
95
+ grounding=grounding,
96
+ confidence=confidence,
97
+ confidence_why=confidence_why,
98
+ max_retrieved_observation_chars=max_retrieved_observation_chars,
99
+ )