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/harness/skills.py ADDED
@@ -0,0 +1,116 @@
1
+ """Skills: reusable techniques a harness ships, one `SKILL.md`-style unit each.
2
+
3
+ A skill is one markdown file with tiny frontmatter (`name`, `description`) and a body. Harnesses
4
+ surface skills by **progressive disclosure**: only the name+description index is preloaded into the
5
+ system prompt (`render_index`); the agent pulls a full body on demand with the `read_skill` tool,
6
+ so always-loaded context stays small while the library grows.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from pathlib import Path
13
+
14
+ from pydantic import BaseModel, field_validator
15
+
16
+ _NAME_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
17
+ _FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---\n(.*)$", re.DOTALL)
18
+
19
+
20
+ class Skill(BaseModel):
21
+ """One reusable skill: a name, a one-line trigger description, and a body to reuse.
22
+
23
+ Validation lives on the MODEL, not just the markdown parser: skill names become file paths
24
+ (`skills/<name>.md`), so a programmatically-constructed skill with a hostile name (`../../x`)
25
+ must be impossible, not merely improbable. The description is coerced to one line (it lives in
26
+ frontmatter, where a newline silently truncates on round-trip); the body is stripped so
27
+ round-trips compare equal.
28
+ """
29
+
30
+ name: str
31
+ description: str
32
+ body: str
33
+
34
+ @field_validator("name")
35
+ @classmethod
36
+ def _kebab_name(cls, v: str) -> str:
37
+ if not _NAME_RE.match(v):
38
+ raise ValueError(f"skill name {v!r} must be kebab-case ([a-z0-9-])")
39
+ return v
40
+
41
+ @field_validator("description")
42
+ @classmethod
43
+ def _one_line(cls, v: str) -> str:
44
+ return " ".join(v.split())
45
+
46
+ @field_validator("body")
47
+ @classmethod
48
+ def _stripped(cls, v: str) -> str:
49
+ return v.strip()
50
+
51
+ def to_markdown(self) -> str:
52
+ """Serialize to a SKILL.md-style file (frontmatter + body)."""
53
+ return f"---\nname: {self.name}\ndescription: {self.description}\n---\n{self.body}"
54
+
55
+ @classmethod
56
+ def from_markdown(cls, text: str) -> Skill:
57
+ match = _FRONTMATTER_RE.match(text)
58
+ if match is None:
59
+ raise ValueError("skill file has no frontmatter")
60
+ meta = parse_frontmatter(match.group(1))
61
+ name = meta.get("name", "")
62
+ if not _NAME_RE.match(name):
63
+ raise ValueError(f"skill name {name!r} must be kebab-case")
64
+ return cls(name=name, description=meta.get("description", ""), body=match.group(2))
65
+
66
+
67
+ def parse_frontmatter(block: str) -> dict[str, str]:
68
+ """Parse the tiny `key: value` frontmatter (one field per line)."""
69
+ out: dict[str, str] = {}
70
+ for line in block.splitlines():
71
+ key, sep, value = line.partition(":")
72
+ if sep:
73
+ out[key.strip()] = value.strip()
74
+ return out
75
+
76
+
77
+ class SkillLibrary:
78
+ """An in-memory set of skills, loadable from / writable to a `skills/` directory."""
79
+
80
+ def __init__(self, skills: list[Skill] | None = None) -> None:
81
+ self._skills: dict[str, Skill] = {s.name: s for s in (skills or [])}
82
+
83
+ @classmethod
84
+ def from_dir(cls, root: str | Path) -> SkillLibrary:
85
+ """Load every `*.md` under `root` (a malformed skill file is an error, not skipped —
86
+ a harness that ships a broken skill should fail loudly at load time, not at rollout)."""
87
+ library = cls()
88
+ root = Path(root)
89
+ if not root.exists():
90
+ return library
91
+ for path in sorted(root.glob("*.md")):
92
+ skill = Skill.from_markdown(path.read_text(encoding="utf-8"))
93
+ library._skills[skill.name] = skill
94
+ return library
95
+
96
+ def write_dir(self, root: str | Path) -> None:
97
+ """Write one `<name>.md` per skill under `root` (created if missing)."""
98
+ root = Path(root)
99
+ root.mkdir(parents=True, exist_ok=True)
100
+ for skill in self._skills.values():
101
+ (root / f"{skill.name}.md").write_text(skill.to_markdown(), encoding="utf-8")
102
+
103
+ def __len__(self) -> int:
104
+ return len(self._skills)
105
+
106
+ def names(self) -> list[str]:
107
+ return sorted(self._skills)
108
+
109
+ def get(self, name: str) -> Skill | None:
110
+ return self._skills.get(name)
111
+
112
+ def render_index(self) -> str:
113
+ """Progressive-disclosure index: name + description only (bodies via read_skill)."""
114
+ if not self._skills:
115
+ return ""
116
+ return "\n".join(f"- {s.name}: {s.description}" for s in self._skills.values())
@@ -0,0 +1,319 @@
1
+ """Portable source trees for editing and reconstructing complete harnesses."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import tomllib
7
+ from pathlib import PurePosixPath
8
+
9
+ import tomli_w
10
+ from pydantic import BaseModel, ConfigDict, field_validator, model_validator
11
+
12
+ from wmo.core.text import validate_durable_text
13
+
14
+ # _SAFE_PATH_RE and _SLUG_RE are doc's path and surface-id grammars; sharing them keeps this
15
+ # module's file checks and Surface validation from ever drifting apart.
16
+ from wmo.harness.doc import (
17
+ _SAFE_PATH_RE,
18
+ _SLUG_RE,
19
+ _STORE_METADATA_FILES,
20
+ CODE_RUNTIME_ID,
21
+ MAX_OUTPUT_TOKENS_ID,
22
+ MAX_SURFACE_PATH_BYTES,
23
+ MAX_TURNS_ID,
24
+ RUNTIME_KIND_ID,
25
+ TEMPERATURE_ID,
26
+ TOOL_POLICY_ID,
27
+ HarnessDoc,
28
+ Surface,
29
+ SurfaceKind,
30
+ code_surface_id,
31
+ )
32
+ from wmo.harness.skills import Skill
33
+
34
+ SYSTEM_FILE = "SYSTEM.md"
35
+ CONFIG_FILE = "config.toml"
36
+ RUNTIME_FILE = "runtime.py"
37
+ SKILLS_DIR = "skills"
38
+
39
+ _RESERVED_RENDER_FILES = frozenset({SYSTEM_FILE, CONFIG_FILE, RUNTIME_FILE})
40
+ _TREE_HASH_BYTES = 16
41
+ MAX_SOURCE_PATH_BYTES = MAX_SURFACE_PATH_BYTES
42
+
43
+
44
+ class HarnessSourceFile(BaseModel):
45
+ """One UTF-8 text file at a canonical path inside a harness source tree."""
46
+
47
+ model_config = ConfigDict(frozen=True)
48
+
49
+ path: str
50
+ content: str
51
+
52
+ @model_validator(mode="after")
53
+ def _validate_file(self) -> HarnessSourceFile:
54
+ candidate = PurePosixPath(self.path)
55
+ if (
56
+ candidate.is_absolute()
57
+ or not candidate.parts
58
+ or candidate.as_posix() != self.path
59
+ or ".." in candidate.parts
60
+ or not _SAFE_PATH_RE.fullmatch(self.path)
61
+ or len(self.path.encode("utf-8")) > MAX_SOURCE_PATH_BYTES
62
+ ):
63
+ raise ValueError(f"source path {self.path!r} must be a canonical relative POSIX path")
64
+ validate_durable_text(self.content, field=f"source file {self.path!r}")
65
+ return self
66
+
67
+
68
+ class HarnessSourceTree(BaseModel):
69
+ """A complete editable file representation that can be parsed into a HarnessDoc.
70
+
71
+ Round-tripping through `from_doc`/`to_doc` is lossless only on the canonical subset: a
72
+ multi-prompt document renders as one SYSTEM.md and reparses as a single `prompt:core`
73
+ surface, and validation-only `Surface.budget` values are dropped.
74
+ """
75
+
76
+ model_config = ConfigDict(frozen=True)
77
+
78
+ files: tuple[HarnessSourceFile, ...]
79
+
80
+ @field_validator("files")
81
+ @classmethod
82
+ def _canonical_files(
83
+ cls, files: tuple[HarnessSourceFile, ...]
84
+ ) -> tuple[HarnessSourceFile, ...]:
85
+ paths = [item.path for item in files]
86
+ duplicates = sorted({path for path in paths if paths.count(path) > 1})
87
+ if duplicates:
88
+ raise ValueError(f"duplicate source path(s): {duplicates}")
89
+ metadata = sorted(path for path in paths if path in _STORE_METADATA_FILES)
90
+ if metadata:
91
+ raise ValueError(f"source tree cannot contain store metadata file(s): {metadata}")
92
+ # Renders land on real filesystems, so structural conflicts must fail here, at
93
+ # validation time, not later inside a store write.
94
+ by_fold: dict[str, str] = {}
95
+ for path in sorted(paths):
96
+ claimed = by_fold.setdefault(path.casefold(), path)
97
+ if claimed != path:
98
+ raise ValueError(
99
+ f"source paths {claimed!r} and {path!r} differ only by letter case and "
100
+ "would collide on a case-insensitive filesystem; rename one"
101
+ )
102
+ directory_prefixes: dict[str, str] = {}
103
+ for path in paths:
104
+ parts = path.split("/")
105
+ for index in range(1, len(parts)):
106
+ directory_prefixes.setdefault("/".join(parts[:index]), path)
107
+ for path in sorted(paths):
108
+ child = directory_prefixes.get(path)
109
+ if child is not None:
110
+ raise ValueError(
111
+ f"source path {path!r} is a file but is also the directory holding "
112
+ f"{child!r}; rename one so no file path is a directory prefix of another"
113
+ )
114
+ return tuple(sorted(files, key=lambda item: item.path))
115
+
116
+ @classmethod
117
+ def from_doc(cls, doc: HarnessDoc) -> HarnessSourceTree:
118
+ """Render a harness into the exact source files an external editor should see."""
119
+ files: dict[str, str] = {
120
+ SYSTEM_FILE: doc.system_prompt(),
121
+ CONFIG_FILE: tomli_w.dumps(
122
+ {
123
+ "harness": {
124
+ "tools": doc.tools(),
125
+ "max_turns": doc.max_turns(),
126
+ "max_output_tokens": doc.max_output_tokens(),
127
+ "temperature": doc.temperature(),
128
+ "runtime_kind": doc.runtime_kind(),
129
+ }
130
+ }
131
+ ),
132
+ }
133
+ for skill in doc.skills():
134
+ files[f"{SKILLS_DIR}/{skill.name}.md"] = skill.to_markdown()
135
+ runtime = doc.surface(CODE_RUNTIME_ID)
136
+ if runtime is not None:
137
+ files[RUNTIME_FILE] = runtime.content
138
+ for surface in doc.code_files():
139
+ assert surface.path is not None
140
+ if surface.path in _RESERVED_RENDER_FILES or surface.path.startswith(f"{SKILLS_DIR}/"):
141
+ raise ValueError(
142
+ f"code surface path {surface.path!r} collides with a reserved file"
143
+ )
144
+ files[surface.path] = surface.content
145
+ return cls(
146
+ files=tuple(
147
+ HarnessSourceFile(path=path, content=content) for path, content in files.items()
148
+ )
149
+ )
150
+
151
+ def to_doc(self, name: str) -> HarnessDoc:
152
+ """Parse the complete source tree into one validated harness document."""
153
+ files = self.file_map()
154
+ if SYSTEM_FILE not in files:
155
+ raise ValueError(f"harness source tree is missing required {SYSTEM_FILE}")
156
+ surfaces = [
157
+ Surface(
158
+ id="prompt:core",
159
+ kind=SurfaceKind.PROMPT,
160
+ content=files[SYSTEM_FILE],
161
+ )
162
+ ]
163
+ config_text = files.get(CONFIG_FILE)
164
+ if config_text is not None:
165
+ parsed_config = tomllib.loads(config_text)
166
+ unknown_tables = sorted(set(parsed_config) - {"harness"})
167
+ if unknown_tables:
168
+ raise ValueError(
169
+ f"{CONFIG_FILE} contains unknown top-level table(s): {unknown_tables}"
170
+ )
171
+ config = parsed_config.get("harness", {})
172
+ if not isinstance(config, dict):
173
+ raise ValueError(f"{CONFIG_FILE} [harness] must be a table")
174
+ allowed_fields = {
175
+ "tools",
176
+ "max_turns",
177
+ "max_output_tokens",
178
+ "temperature",
179
+ "runtime_kind",
180
+ }
181
+ unknown_fields = sorted(set(config) - allowed_fields)
182
+ if unknown_fields:
183
+ raise ValueError(
184
+ f"{CONFIG_FILE} [harness] contains unknown field(s): {unknown_fields}"
185
+ )
186
+ tools = config.get("tools")
187
+ if tools is not None:
188
+ if not isinstance(tools, list) or not all(isinstance(item, str) for item in tools):
189
+ raise ValueError(f"{CONFIG_FILE} harness.tools must be an array of strings")
190
+ surfaces.append(
191
+ Surface(
192
+ id=TOOL_POLICY_ID,
193
+ kind=SurfaceKind.TOOL_POLICY,
194
+ content="\n".join(tools),
195
+ )
196
+ )
197
+ scalar_fields = (
198
+ ("max_turns", MAX_TURNS_ID),
199
+ ("max_output_tokens", MAX_OUTPUT_TOKENS_ID),
200
+ ("temperature", TEMPERATURE_ID),
201
+ )
202
+ for field, surface_id in scalar_fields:
203
+ if field in config:
204
+ surfaces.append(
205
+ Surface(
206
+ id=surface_id,
207
+ kind=SurfaceKind.PARAM,
208
+ content=str(config[field]),
209
+ )
210
+ )
211
+ runtime_kind = config.get("runtime_kind")
212
+ if runtime_kind is not None and not isinstance(runtime_kind, str):
213
+ raise ValueError(f"{CONFIG_FILE} harness.runtime_kind must be a string")
214
+ if runtime_kind and runtime_kind != "kit-python":
215
+ surfaces.append(
216
+ Surface(
217
+ id=RUNTIME_KIND_ID,
218
+ kind=SurfaceKind.PARAM,
219
+ content=str(runtime_kind),
220
+ )
221
+ )
222
+ runtime = files.get(RUNTIME_FILE)
223
+ if runtime is not None:
224
+ surfaces.append(Surface(id=CODE_RUNTIME_ID, kind=SurfaceKind.CODE, content=runtime))
225
+ for item in self.files:
226
+ path = PurePosixPath(item.path)
227
+ if path.parts[0] != SKILLS_DIR:
228
+ continue
229
+ if len(path.parts) != 2 or path.suffix != ".md":
230
+ raise ValueError(f"skill source path {item.path!r} must be skills/<skill-name>.md")
231
+ skill = Skill.from_markdown(item.content)
232
+ if path.stem != skill.name:
233
+ raise ValueError(
234
+ f"skill source path {item.path!r} does not match declared name {skill.name!r}"
235
+ )
236
+ surfaces.append(
237
+ Surface(
238
+ id=f"skill:{skill.name}",
239
+ kind=SurfaceKind.SKILL,
240
+ content=skill.to_markdown(),
241
+ )
242
+ )
243
+ code_paths_by_id: dict[str, str] = {}
244
+ for item in self.files:
245
+ if item.path in _RESERVED_RENDER_FILES or item.path.startswith(f"{SKILLS_DIR}/"):
246
+ continue
247
+ surface_id = _code_surface_id(item.path)
248
+ if surface_id == CODE_RUNTIME_ID:
249
+ raise ValueError(
250
+ f"code file path {item.path!r} would alias the reserved in-process runtime "
251
+ f"surface {CODE_RUNTIME_ID!r} (which renders as {RUNTIME_FILE}); rename "
252
+ "the file"
253
+ )
254
+ claimed = code_paths_by_id.get(surface_id)
255
+ if claimed is not None:
256
+ raise ValueError(
257
+ f"code file paths {claimed!r} and {item.path!r} both map to surface id "
258
+ f"{surface_id!r} ('/' and '.' both become '-'); rename one so every "
259
+ "path keeps a distinct id"
260
+ )
261
+ code_paths_by_id[surface_id] = item.path
262
+ surfaces.append(
263
+ Surface(
264
+ id=surface_id,
265
+ kind=SurfaceKind.CODE,
266
+ path=item.path,
267
+ content=item.content,
268
+ )
269
+ )
270
+ return HarnessDoc(name=name, surfaces=surfaces)
271
+
272
+ def file_map(self) -> dict[str, str]:
273
+ """Return a new path-to-content mapping in canonical path order."""
274
+ return {item.path: item.content for item in self.files}
275
+
276
+ @property
277
+ def total_bytes(self) -> int:
278
+ """Return the UTF-8 payload size across all source files."""
279
+ return sum(len(item.content.encode("utf-8")) for item in self.files)
280
+
281
+ @property
282
+ def tree_hash(self) -> str:
283
+ """Return a content address covering every source path and byte."""
284
+ digest = hashlib.blake2b(digest_size=_TREE_HASH_BYTES)
285
+ for item in self.files:
286
+ path = item.path.encode("utf-8")
287
+ content = item.content.encode("utf-8")
288
+ digest.update(len(path).to_bytes(4, "big"))
289
+ digest.update(path)
290
+ digest.update(len(content).to_bytes(8, "big"))
291
+ digest.update(content)
292
+ return digest.hexdigest()
293
+
294
+ def validate_bounds(self, *, max_files: int, max_bytes: int) -> None:
295
+ """Reject a tree that exceeds explicit file-count or UTF-8 byte bounds."""
296
+ if isinstance(max_files, bool) or not isinstance(max_files, int) or max_files < 1:
297
+ raise ValueError("max_files must be a positive integer")
298
+ if isinstance(max_bytes, bool) or not isinstance(max_bytes, int) or max_bytes < 1:
299
+ raise ValueError("max_bytes must be a positive integer")
300
+ if len(self.files) > max_files:
301
+ raise ValueError(
302
+ f"source tree has {len(self.files)} files, more than {max_files} files allowed"
303
+ )
304
+ if self.total_bytes > max_bytes:
305
+ raise ValueError(
306
+ f"source tree has {self.total_bytes} bytes, more than {max_bytes} bytes allowed"
307
+ )
308
+
309
+
310
+ def _code_surface_id(path: str) -> str:
311
+ """Derive a code file's surface id, failing with the path and the allowed grammar."""
312
+ surface_id = code_surface_id(path)
313
+ if not _SLUG_RE.fullmatch(surface_id.partition(":")[2]):
314
+ raise ValueError(
315
+ f"code file path {path!r} maps to surface id {surface_id!r}, which is not a valid "
316
+ "'code:<kebab-slug>' id; use lowercase [a-z0-9] runs separated by single '/', '.', "
317
+ "or '-' characters (for example src/agent-loop.ts)"
318
+ )
319
+ return surface_id
wmo/harness/store.py ADDED
@@ -0,0 +1,176 @@
1
+ """Harnesses on disk: immutable numbered versions plus movable aliases, per name.
2
+
3
+ Like world models under `.wmo/models/<name>/`, a harness is a named artifact — but harnesses
4
+ accumulate *versions*, because they are the thing `wmo optimize harness` iterates on. A
5
+ version, once
6
+ written, never changes; deployment state lives in movable aliases; and every eval result keys to an
7
+ immutable version rather than to "whatever the harness currently is".
8
+
9
+ .wmo/harnesses/<name>/
10
+ aliases.toml # [aliases] champion = 3 (movable pointers; rollback = re-point)
11
+ v1/
12
+ doc.json # the authoritative HarnessDoc serialization
13
+ SYSTEM.md # rendered export of the same document, for running the harness
14
+ config.toml # outside wmo — regenerated on every save, never read back
15
+ skills/<slug>.md # when doc.json is present
16
+ v3/ ...
17
+
18
+ `doc.json` is authoritative; the rendered files are an export. A directory with rendered files but
19
+ no `doc.json` (a hand-authored harness) still loads: the files parse into a single-prompt document.
20
+ Rendered-only loads are strict: unknown config.toml tables or fields, non-.md files under skills/,
21
+ and a skill filename that does not match its frontmatter name are errors with actionable messages,
22
+ not silently ignored. Dotfiles (.DS_Store and friends) are skipped.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import tomllib
28
+ from pathlib import Path
29
+
30
+ import tomli_w
31
+
32
+ from wmo.config.store import validate_name
33
+ from wmo.harness.doc import HarnessDoc
34
+ from wmo.harness.source_tree import SYSTEM_FILE, HarnessSourceFile, HarnessSourceTree
35
+
36
+ HARNESSES_DIR = "harnesses"
37
+ CHAMPION_ALIAS = "champion"
38
+
39
+ _DOC_FILE = "doc.json"
40
+ _ALIASES_FILE = "aliases.toml"
41
+
42
+
43
+ class HarnessStore:
44
+ """Named, versioned harnesses under `<root>/harnesses/<name>/`."""
45
+
46
+ def __init__(self, root: str | Path = ".wmo") -> None:
47
+ self.root = Path(root)
48
+
49
+ @property
50
+ def harnesses_dir(self) -> Path:
51
+ return self.root / HARNESSES_DIR
52
+
53
+ def dir_for(self, name: str) -> Path:
54
+ return self.harnesses_dir / validate_name(name)
55
+
56
+ # -- enumeration -------------------------------------------------------------------------
57
+
58
+ def list_names(self) -> list[str]:
59
+ if not self.harnesses_dir.exists():
60
+ return []
61
+ return sorted(
62
+ d.name for d in self.harnesses_dir.iterdir() if d.is_dir() and self.versions(d.name)
63
+ )
64
+
65
+ def versions(self, name: str) -> list[int]:
66
+ directory = self.dir_for(name)
67
+ if not directory.exists():
68
+ return []
69
+ found: list[int] = []
70
+ for child in directory.iterdir():
71
+ if child.is_dir() and child.name.startswith("v") and child.name[1:].isdigit():
72
+ found.append(int(child.name[1:]))
73
+ return sorted(found)
74
+
75
+ def exists(self, name: str) -> bool:
76
+ return bool(self.versions(name))
77
+
78
+ # -- aliases -----------------------------------------------------------------------------
79
+
80
+ def aliases(self, name: str) -> dict[str, int]:
81
+ path = self.dir_for(name) / _ALIASES_FILE
82
+ if not path.exists():
83
+ return {}
84
+ data = tomllib.loads(path.read_text(encoding="utf-8")).get("aliases", {})
85
+ return {k: v for k, v in data.items() if isinstance(v, int)}
86
+
87
+ def set_alias(self, name: str, alias: str, version: int) -> None:
88
+ """Point `alias` at `version` (moving it if it exists). Rollback is re-pointing."""
89
+ if version not in self.versions(name):
90
+ raise ValueError(f"harness {name!r} has no version v{version}")
91
+ current = self.aliases(name)
92
+ current[alias] = version
93
+ path = self.dir_for(name) / _ALIASES_FILE
94
+ path.write_text(tomli_w.dumps({"aliases": current}), encoding="utf-8")
95
+
96
+ # -- load / save ---------------------------------------------------------------------------
97
+
98
+ def resolve_version(self, name: str, ref: str | None = None) -> int:
99
+ """Resolve a version ref: `None` -> champion alias, else latest; `"vN"`/`"N"`; an alias."""
100
+ available = self.versions(name)
101
+ if not available:
102
+ raise FileNotFoundError(
103
+ f"no harness named {name!r} under {self.harnesses_dir} "
104
+ f"(have: {', '.join(self.list_names()) or 'none'})"
105
+ )
106
+ aliases = self.aliases(name)
107
+ if ref is None:
108
+ return aliases.get(CHAMPION_ALIAS, available[-1])
109
+ normalized = ref.removeprefix("v")
110
+ if normalized.isdigit():
111
+ version = int(normalized)
112
+ if version not in available:
113
+ raise ValueError(f"harness {name!r} has no version v{version}")
114
+ return version
115
+ if ref in aliases:
116
+ return aliases[ref]
117
+ raise ValueError(f"harness {name!r} has no version or alias {ref!r}")
118
+
119
+ def load(self, name: str, ref: str | None = None) -> HarnessDoc:
120
+ version = self.resolve_version(name, ref)
121
+ directory = self.dir_for(name) / f"v{version}"
122
+ doc_path = directory / _DOC_FILE
123
+ if doc_path.exists():
124
+ doc = HarnessDoc.model_validate_json(doc_path.read_text(encoding="utf-8"))
125
+ else:
126
+ doc = _parse_rendered(name, directory)
127
+ return doc.model_copy(update={"name": name, "version": version})
128
+
129
+ def save_version(self, doc: HarnessDoc, *, alias: str | None = None) -> HarnessDoc:
130
+ """Write `doc` as the next version of its name; optionally point `alias` at it.
131
+
132
+ Versions are append-only: this never touches an existing version directory.
133
+ """
134
+ validate_name(doc.name)
135
+ version = (self.versions(doc.name)[-1] + 1) if self.exists(doc.name) else 1
136
+ stamped = doc.model_copy(update={"version": version})
137
+ directory = self.dir_for(doc.name) / f"v{version}"
138
+ directory.mkdir(parents=True, exist_ok=False) # append-only: collision is a bug
139
+ (directory / _DOC_FILE).write_text(stamped.model_dump_json(indent=2), encoding="utf-8")
140
+ for rel_path, content in _render(stamped).items():
141
+ target = directory / rel_path
142
+ target.parent.mkdir(parents=True, exist_ok=True)
143
+ target.write_text(content, encoding="utf-8")
144
+ if alias is not None:
145
+ self.set_alias(doc.name, alias, version)
146
+ return stamped
147
+
148
+
149
+ def _render(doc: HarnessDoc) -> dict[str, str]:
150
+ """Render the document to its file export (relative path -> content)."""
151
+ return HarnessSourceTree.from_doc(doc).file_map()
152
+
153
+
154
+ def _parse_rendered(name: str, directory: Path) -> HarnessDoc:
155
+ """Parse a rendered/hand-authored directory (no doc.json) into a document.
156
+
157
+ The whole `SYSTEM.md` becomes one `prompt:core` surface — section boundaries are not
158
+ recoverable from a rendered prompt, which is exactly why `doc.json` is the authoritative form.
159
+ """
160
+ files: list[HarnessSourceFile] = []
161
+ for path in sorted(directory.rglob("*")):
162
+ if not path.is_file():
163
+ continue
164
+ rel_path = path.relative_to(directory)
165
+ if any(part.startswith(".") for part in rel_path.parts):
166
+ # Finder and editors drop metadata like .DS_Store (often with NUL bytes) into
167
+ # hand-authored dirs. A dotfile can never be a harness surface (its code_surface_id
168
+ # is not a valid slug), so skip it instead of failing the load on its content.
169
+ continue
170
+ rel = rel_path.as_posix()
171
+ if rel in {_DOC_FILE, _ALIASES_FILE}:
172
+ continue
173
+ files.append(HarnessSourceFile(path=rel, content=path.read_text(encoding="utf-8")))
174
+ if not files:
175
+ raise ValueError(f"harness dir {directory} has neither {_DOC_FILE} nor {SYSTEM_FILE}")
176
+ return HarnessSourceTree(files=tuple(files)).to_doc(name)