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/core/render.py ADDED
@@ -0,0 +1,271 @@
1
+ """Canonical text rendering of states, actions, and steps.
2
+
3
+ These are the *single* source of truth for turning the core types into prompt/embedding text. The
4
+ retriever embeds `encode_state_action` (phi in DreamGym Eq. 4), the world-model engine and the GEPA
5
+ optimizer both render the env prompt from the same helpers, and demos render via `render_demo`.
6
+
7
+ Keeping this in `wmo.core` (which depends on nothing) lets engine, optimize, and retrieval all share
8
+ one rendering without an import cycle — so a step embedded for retrieval and the same step shown to
9
+ the model as a demo are described identically.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+
16
+ from wmo.core.types import Action, EnvState, JsonObject, Step
17
+
18
+
19
+ def render_json(value: JsonObject) -> str:
20
+ """Stable, compact one-liner for a JSON object: sorted keys, no whitespace churn.
21
+
22
+ Sorting makes semantically equal objects render byte-identically regardless of insertion order,
23
+ which is what keeps cosine similarity (and cross-run prompt text) meaningful.
24
+ """
25
+ if not value:
26
+ return "{}"
27
+ return json.dumps(value, sort_keys=True, separators=(",", ":"), default=str)
28
+
29
+
30
+ def render_action(action: Action) -> str:
31
+ """One-line rendering of an action: tool call (name + args) or a free-text message."""
32
+ if action.kind.value == "tool_call":
33
+ name = action.name or "(unnamed)"
34
+ return f"tool_call {name}({render_json(action.arguments)})"
35
+ return f"message: {action.content or ''}"
36
+
37
+
38
+ def encode_action(action: Action) -> str:
39
+ """Command-only retrieval key: the action itself (tool + arguments, or message) with none of the
40
+ `STATE:` / `ACTION kind=` scaffolding `encode_state_action` adds.
41
+
42
+ For stateless traces (empty env state) that scaffolding is constant across every step, so it
43
+ dominates the embedding and dilutes the part that actually varies — the command. Embedding just
44
+ the action concentrates the signal, which helps a semantic embedder find same-intent neighbours.
45
+ """
46
+ # Truthy checks (not `is not None`): a blank name/content contributes no retrieval signal, and
47
+ # an all-blank action would otherwise embed as an empty string — indistinguishable from every
48
+ # other blank step. Fall back to `render_action` (labelled, never empty) in that case.
49
+ parts: list[str] = []
50
+ if action.name:
51
+ parts.append(action.name)
52
+ if action.arguments:
53
+ parts.append(render_json(action.arguments))
54
+ if action.content:
55
+ parts.append(action.content)
56
+ return " ".join(parts) if parts else render_action(action)
57
+
58
+
59
+ def encode_state_action(state: EnvState, action: Action) -> str:
60
+ """Render (state, action) into the text embedded for phi(s, a) and reused in prompts.
61
+
62
+ A labelled, line-oriented structured summary: env state (structured config + scratchpad
63
+ "database") then the action (kind, tool name, arguments, message). Empty fields are omitted so
64
+ equal steps render identically.
65
+ """
66
+ lines = ["STATE:", f" structured: {render_json(state.structured)}"]
67
+ if state.scratchpad:
68
+ lines.append(f" scratchpad: {state.scratchpad}")
69
+ lines.append(f"ACTION kind={action.kind.value}")
70
+ if action.name is not None:
71
+ lines.append(f" tool: {action.name}")
72
+ if action.arguments:
73
+ lines.append(f" arguments: {render_json(action.arguments)}")
74
+ if action.content is not None:
75
+ lines.append(f" message: {action.content}")
76
+ return "\n".join(lines)
77
+
78
+
79
+ def render_demo(step: Step, *, max_observation_chars: int | None = None) -> str:
80
+ """Render a retrieved past step as a (state, action) -> observation few-shot example.
81
+
82
+ `max_observation_chars`, when set, keeps only the first N characters of the observation and
83
+ appends a "… [+N chars]" marker. Retrieval keys on (state, action), so the *format* and salient
84
+ head of a past observation carry the signal; capping the tail bounds prompt growth when many or
85
+ large past examples are retrieved (a big `top_k` over verbose shell/log output crowds context).
86
+ """
87
+ obs = step.observation
88
+ content = obs.content
89
+ if max_observation_chars is not None and len(content) > max_observation_chars:
90
+ dropped = len(content) - max_observation_chars
91
+ content = f"{content[:max_observation_chars]}… [+{dropped} chars]"
92
+ return (
93
+ f"{encode_state_action(step.state_before, step.action)}\n"
94
+ f"OBSERVATION (is_error={obs.is_error}): {content}"
95
+ )
96
+
97
+
98
+ def build_env_prompt(
99
+ base_prompt: str,
100
+ task: str | None,
101
+ state: EnvState,
102
+ action: Action,
103
+ *,
104
+ history: list[Step] | None = None,
105
+ demos: list[Step] | None = None,
106
+ knowledge: str | None = None,
107
+ reasoning: bool = False,
108
+ grounding: bool = False,
109
+ confidence: bool = False,
110
+ confidence_why: bool = False,
111
+ max_retrieved_observation_chars: int | None = None,
112
+ ) -> tuple[str, str]:
113
+ """Assemble the (system, user) world-model completion that predicts the next observation.
114
+
115
+ Mirrors DreamGym Eq. 4 ``M_exp(R_t | {(s_i,a_i)}, {d_j}, tau)``: the base/optimized prompt is
116
+ the system message; the task, current state, recent history, retrieved demos, and the incoming
117
+ action form the user message. This is the *single* assembly used by both the serving engine
118
+ (`wmo.engine.prompts`) and the GEPA optimizer, so prompts are evolved against exactly what the
119
+ world model serves.
120
+
121
+ `knowledge`/`reasoning`/`grounding`/`confidence` are the opt-in agentic-mode extensions; at
122
+ their defaults the rendering is byte-identical to the pre-knowledge shape (pinned in
123
+ render_test), so prebuilt models keep serving unchanged. `knowledge` (the cross-session
124
+ knowledge base, rendered by `wmo.engine.knowledge`) becomes an authoritative facts section;
125
+ `reasoning` switches the output contract to deliberate-then-answer; `grounding` offers the
126
+ `ground_query` escape hatch (pass it only when a live grounder will actually serve it).
127
+ """
128
+ system = base_prompt
129
+ demo_block = (
130
+ "\n\n".join(
131
+ render_demo(d, max_observation_chars=max_retrieved_observation_chars) for d in demos
132
+ )
133
+ if demos
134
+ else "(no similar past examples)"
135
+ )
136
+ history_block = (
137
+ "\n".join(
138
+ f"{encode_state_action(h.state_before, h.action)}\n"
139
+ f"OBSERVATION (is_error={h.observation.is_error}): {h.observation.content}"
140
+ for h in history
141
+ )
142
+ if history
143
+ else "(start of session)"
144
+ )
145
+ knowledge_block = (
146
+ "KNOWLEDGE BASE (canonical facts about this environment — authoritative over your priors;"
147
+ f" entities, rules, schemas, and state-dependent gates):\n{knowledge}\n\n"
148
+ if knowledge
149
+ else ""
150
+ )
151
+ user = (
152
+ f"TASK:\n{task or '(none)'}\n\n"
153
+ f"{knowledge_block}"
154
+ f"INTERACTION HISTORY:\n{history_block}\n\n"
155
+ f"SIMILAR PAST EXAMPLES:\n{demo_block}\n\n"
156
+ f"CURRENT ENV STATE:\n structured: {render_json(state.structured)}\n"
157
+ f" scratchpad: {state.scratchpad or '(empty)'}\n\n"
158
+ f"AGENT ACTION:\n{render_action(action)}\n\n"
159
+ + output_contract(
160
+ reasoning=reasoning,
161
+ grounding=grounding,
162
+ confidence=confidence,
163
+ confidence_why=confidence_why,
164
+ )
165
+ )
166
+ return system, user
167
+
168
+
169
+ # The world-model output contract. Parsed by `wmo.core.parsing.parse_observation`; kept next to the
170
+ # prompt assembly so the instruction and the parser never drift.
171
+ OUTPUT_CONTRACT = (
172
+ "Respond with ONLY a JSON object describing the environment's response to this action:\n"
173
+ '{"output": "<exactly what the environment returns to the agent>", '
174
+ '"is_error": <true if the action failed/was invalid>, '
175
+ '"state_note": "<one short fact to remember about the new env state, or empty>"}'
176
+ )
177
+
178
+ # Reasoning-mode contract pieces. `reasoning` MUST be the first key: decoding is ordered, so
179
+ # putting the deliberation before `output` is what makes it an actual deliberation rather than a
180
+ # post-hoc rationalization. `kb_note` is the cross-session counterpart of `state_note` (persisted
181
+ # to the knowledge base by the engine); `ground_query` is offered only when a grounder is active.
182
+ # Deliberation instruction, tuned on observed live failures (.agents/docs/research/
183
+ # agentic_results inspection):
184
+ # unbounded deliberations blew the token budget and truncated the output (hence "1-4 short
185
+ # sentences"); agent-side policy was mistaken for an env gate (a cancel the policy forbids still
186
+ # EXECUTES — tools are mechanical); unobserved state was assumed ("already installed");
187
+ # exploratory greps/finds were assumed to hit because the task narrative implied the target
188
+ # exists; and exact computations missed edge cases (fold/wc off-by-one on a trailing newline).
189
+ # NOTE the search guidance is deliberately evidence-NEUTRAL: a first draft said "searches miss
190
+ # often — predict empty", derived from a corpus whose empties turned out to be capture junk
191
+ # (D24), and it measurably hurt on the clean corpus. Bias neither way; follow the evidence.
192
+ _REASONING_FIELD = (
193
+ '{"reasoning": "<1-4 short sentences BEFORE deciding the output: what would this action'
194
+ " really do given the current state? Check the gates the ENVIRONMENT itself enforces (auth"
195
+ " checks, availability, preconditions, timeouts) against the knowledge base, history, and"
196
+ " examples — policy the AGENT is merely supposed to follow does not make a tool refuse, so"
197
+ " predict refusal only where the environment demonstrably enforces it. Never assume"
198
+ " unobserved state (installed packages, existing files) — default to a fresh environment."
199
+ " Searches and reads (grep/find for guessed strings, line ranges that may exceed the file)"
200
+ " can genuinely miss: decide hit vs. miss from the evidence and from how similar commands"
201
+ " behaved in the examples, not from what the task narrative hopes is there. Work exact"
202
+ ' computations carefully (counts, off-by-one, trailing newlines)>", '
203
+ )
204
+ _OUTPUT_IS_ERROR_FIELDS = (
205
+ '"output": "<exactly what the environment returns to the agent>", '
206
+ '"is_error": <true if the action failed/was invalid>'
207
+ )
208
+ _STATE_NOTE_FIELD = (
209
+ ', "state_note": "<one short fact to remember about the new env state, or empty>"'
210
+ )
211
+ # Verbalized-confidence fields (WS-A6, D75). `confidence` sits AFTER output/is_error so ordered
212
+ # decoding conditions it on the answer actually emitted (the post-hoc p(true) framing, better
213
+ # calibrated than prospective confidence). One decimal = 11 levels: enough resolution for a
214
+ # risk-coverage sweep, coarse enough not to invite false precision. The optional `confidence_why`
215
+ # one-liner comes BEFORE the number (justify-then-rate) so the rating conditions on the
216
+ # articulated reason. Stated confidence is analysis-only: the judge and GEPA never see it
217
+ # (guarded by tests in judge_test/gepa_test).
218
+ _CONFIDENCE_WHY_FIELD = (
219
+ ', "confidence_why": "<one short sentence: the strongest reason to trust or doubt the'
220
+ ' "output" above>"'
221
+ )
222
+ _CONFIDENCE_FIELD = (
223
+ ', "confidence": <your probability, 0.0-1.0 with ONE decimal, that the "output" above'
224
+ " matches what the real environment would return for this action>"
225
+ )
226
+ _KB_NOTE_FIELD = (
227
+ ', "kb_note": "<one canonical fact about this environment worth remembering across ALL'
228
+ ' future sessions (an entity that exists, a rule, a schema), or empty>"'
229
+ ', "state_update": "<the REVISED environment profile: what is running/installed/existing'
230
+ " right now, with beliefs this step contradicted removed — the FULL replacement profile,"
231
+ ' or empty to keep the previous one>"'
232
+ )
233
+ _GROUND_QUERY_FIELD = (
234
+ ', "ground_query": "<a web search query IF the action references a real-world entity (API,'
235
+ " package, flight, product) you cannot ground in the knowledge base, examples, or history —"
236
+ ' the search runs and you answer again with the results; else empty>"'
237
+ )
238
+
239
+
240
+ def output_contract(
241
+ *,
242
+ reasoning: bool = False,
243
+ grounding: bool = False,
244
+ confidence: bool = False,
245
+ confidence_why: bool = False,
246
+ ) -> str:
247
+ """Return the output-contract instruction for the requested mode.
248
+
249
+ The base contract (all flags off) is exactly `OUTPUT_CONTRACT` — the shape every existing
250
+ model was built against. All variants are parsed by the one lenient
251
+ `wmo.core.parsing.parse_observation`. `confidence` inserts the verbalized-confidence field
252
+ after `is_error`; `confidence_why` (a no-op without `confidence`) prepends its one-line
253
+ justification.
254
+ """
255
+ if not reasoning and not grounding and not confidence:
256
+ return OUTPUT_CONTRACT
257
+ conf_fields = ""
258
+ if confidence:
259
+ conf_fields = (_CONFIDENCE_WHY_FIELD if confidence_why else "") + _CONFIDENCE_FIELD
260
+ fields = f"{_OUTPUT_IS_ERROR_FIELDS}{conf_fields}{_STATE_NOTE_FIELD}"
261
+ ground_field = _GROUND_QUERY_FIELD if grounding else ""
262
+ if reasoning:
263
+ return (
264
+ "First deliberate, then answer. Respond with ONLY a JSON object whose FIRST key is"
265
+ f" your deliberation:\n{_REASONING_FIELD}{fields}{_KB_NOTE_FIELD}{ground_field}}}"
266
+ )
267
+ # No deliberation pass: the base fields + whichever optional fields are on.
268
+ return (
269
+ "Respond with ONLY a JSON object describing the environment's response to this action:\n"
270
+ f"{{{fields}{ground_field}}}"
271
+ )
wmo/core/text.py ADDED
@@ -0,0 +1,40 @@
1
+ """Text canonicalization for UTF-8 files, transports, and durable JSON stores."""
2
+
3
+ from __future__ import annotations
4
+
5
+ _REPLACEMENT_CHARACTER = "\N{REPLACEMENT CHARACTER}"
6
+
7
+
8
+ def normalize_durable_text(value: str) -> str:
9
+ """Replace code points that cannot safely cross every WMO persistence boundary.
10
+
11
+ PostgreSQL JSONB rejects embedded NULs and lone UTF-16 surrogates; UTF-8 filesystem and HTTP
12
+ clients reject surrogates as well. A valid surrogate pair is folded into its Unicode scalar.
13
+ """
14
+ normalized: list[str] = []
15
+ index = 0
16
+ while index < len(value):
17
+ code_point = ord(value[index])
18
+ if code_point == 0:
19
+ normalized.append(_REPLACEMENT_CHARACTER)
20
+ elif 0xD800 <= code_point <= 0xDBFF:
21
+ if index + 1 < len(value) and 0xDC00 <= (low := ord(value[index + 1])) <= 0xDFFF:
22
+ scalar = 0x10000 + ((code_point - 0xD800) << 10) + (low - 0xDC00)
23
+ normalized.append(chr(scalar))
24
+ index += 1
25
+ else:
26
+ normalized.append(_REPLACEMENT_CHARACTER)
27
+ elif 0xDC00 <= code_point <= 0xDFFF:
28
+ normalized.append(_REPLACEMENT_CHARACTER)
29
+ else:
30
+ normalized.append(value[index])
31
+ index += 1
32
+ return "".join(normalized)
33
+
34
+
35
+ def validate_durable_text(value: str, *, field: str) -> None:
36
+ """Reject content-addressed text that canonicalization would change."""
37
+ if "\x00" in value:
38
+ raise ValueError(f"{field} contains an embedded NUL character")
39
+ if any(0xD800 <= ord(char) <= 0xDFFF for char in value):
40
+ raise ValueError(f"{field} contains an unpaired UTF-16 surrogate")
wmo/core/types.py ADDED
@@ -0,0 +1,116 @@
1
+ """Core data types shared across the harness.
2
+
3
+ These are the normalized, vendor-agnostic representations that ingestion produces and that the
4
+ WorldModel, retriever, optimizer, and providers all operate on.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from enum import StrEnum
10
+
11
+ from pydantic import BaseModel, Field, JsonValue
12
+
13
+ # Tool arguments, env config, and span metadata are user-defined JSON. `JsonValue` is pydantic's
14
+ # concrete recursive JSON type — honest about the shape without falling back to `Any`.
15
+ JsonObject = dict[str, JsonValue]
16
+
17
+
18
+ class ActionKind(StrEnum):
19
+ TOOL_CALL = "tool_call"
20
+ MESSAGE = "message"
21
+
22
+
23
+ class Action(BaseModel):
24
+ """What the agent did this step. Either a tool call or a free-text message."""
25
+
26
+ kind: ActionKind
27
+ name: str | None = None # tool name, when kind == tool_call
28
+ arguments: JsonObject = Field(default_factory=dict)
29
+ content: str | None = None # message text, when kind == message
30
+
31
+
32
+ class Observation(BaseModel):
33
+ """What the environment returned in response to an action.
34
+
35
+ `reward` is optional and exists to support RL-style use (DreamGym assigns r at terminal steps).
36
+ """
37
+
38
+ content: str
39
+ is_error: bool = False
40
+ reward: float | None = None
41
+ metadata: JsonObject = Field(default_factory=dict)
42
+
43
+
44
+ class EnvState(BaseModel):
45
+ """A snapshot of the environment as seen by the agent.
46
+
47
+ `structured` holds machine-readable env config (cwd, open files, cart contents, ...).
48
+ `scratchpad` is the free-text "database" the world model writes to itself to stay consistent
49
+ across a session (e.g. "user created foo.txt", "logged in as alice").
50
+ """
51
+
52
+ structured: JsonObject = Field(default_factory=dict)
53
+ scratchpad: str = ""
54
+
55
+
56
+ class ErrorClass(StrEnum):
57
+ """Who owns a step's failure: the model (retrainable) or the environment (not)."""
58
+
59
+ CONTROLLABLE = "controllable" # the LLM call itself failed (refusal, bad call, provider error)
60
+ ENVIRONMENTAL = "environmental" # the tool/environment failed executing a well-formed action
61
+
62
+
63
+ class StepAttribution(BaseModel):
64
+ """Per-step provenance of the LLM call behind an action.
65
+
66
+ These fields are what make an ingested corpus policy-trainable: a routing optimizer clusters
67
+ and prices steps by which model produced them, at what token/dollar/latency cost, and whether
68
+ failures were the model's fault. All fields are optional; adapters populate them best-effort
69
+ from whatever the source recorded, and a step with nothing known carries no attribution at all.
70
+ """
71
+
72
+ model: str | None = None # the serving model id (response model preferred over request)
73
+ provider: str | None = None # the model provider/system (e.g. "anthropic", "openai")
74
+ config: JsonObject = Field(
75
+ default_factory=dict
76
+ ) # request params (temperature, max_tokens, ...)
77
+ input_tokens: int | None = None
78
+ output_tokens: int | None = None
79
+ cost_usd: float | None = None
80
+ latency_ms: float | None = None
81
+ error_class: ErrorClass | None = None
82
+ provenance: str | None = None # versioned capture-system marker, when the source carries one
83
+
84
+
85
+ class Step(BaseModel):
86
+ """One (state, action) -> observation transition. The unit of retrieval and scoring."""
87
+
88
+ action: Action
89
+ observation: Observation
90
+ state_before: EnvState = Field(default_factory=EnvState)
91
+ task: str | None = None # originating instruction (tau in DreamGym Eq. 4)
92
+ raw_span_ids: list[str] = Field(default_factory=list)
93
+ attribution: StepAttribution | None = None
94
+
95
+
96
+ class Trace(BaseModel):
97
+ """One full agent session: an ordered list of steps, plus provenance."""
98
+
99
+ trace_id: str
100
+ steps: list[Step] = Field(default_factory=list)
101
+ source: str = "unknown" # vendor name or file path
102
+ metadata: JsonObject = Field(default_factory=dict)
103
+
104
+
105
+ class Session(BaseModel):
106
+ """A live interaction the WorldModel maintains while an agent steps against it."""
107
+
108
+ id: str
109
+ task: str | None = None
110
+ state: EnvState = Field(default_factory=EnvState)
111
+ history: list[Step] = Field(default_factory=list) # {(s_i, a_i)} fed back into the prompt
112
+ # Whether this session's steps enrich the shared retrieval buffer. Serve-time default is True
113
+ # (DreamGym-style online enrichment). Evaluation sessions set False: a closed-loop rollout's
114
+ # PREDICTED steps must not become retrieval demos for later rollouts, or scores become
115
+ # order-dependent and self-reinforcing.
116
+ enrich: bool = True
@@ -0,0 +1,14 @@
1
+ """On-policy distillation optimizer mode.
2
+
3
+ Trains a Tinker LoRA student from rollouts harbor's own terminus-2 agent
4
+ produces on harbor tasks (TerminalBench-2), sampling the student through
5
+ Tinker. The teacher scores the student's sampled tokens via
6
+ compute_logprobs, and the reverse-KL advantages feed the advantage-weighted
7
+ loss `train.loss` names (`importance_sampling` or `ppo`).
8
+ The public surface is deliberately minimal for now: the per-run TOML config
9
+ model and its loader.
10
+ """
11
+
12
+ from wmo.distill.config import DistillConfig, load_distill_config
13
+
14
+ __all__ = ["DistillConfig", "load_distill_config"]
wmo/distill/agents.py ADDED
@@ -0,0 +1,140 @@
1
+ """Harbor agent bridge for distillation rollouts with token-span capture.
2
+
3
+ Harbor instantiates `WmoDistillHarborAgent` (via
4
+ `WMO_DISTILL_HARBOR_AGENT_IMPORT_PATH` plus JSON kwargs) for every distillation
5
+ trial. The one behavioral difference from the base `WmoHarborAgent` is
6
+ provider construction: the worker provider must be the Tinker student, and it
7
+ is built with a `TokenRecorder` that writes the trial's exact sampled token
8
+ spans to `{token_sink_dir}/{trial_name}.jsonl`.
9
+
10
+ The sink lives OUTSIDE harbor's trial directory on purpose: the scorer's
11
+ entry prune deletes invalid trial dirs wholesale before re-running them, and
12
+ a sink inside the trial dir would vanish with it. The rollout collector keys
13
+ the sink dir per training step and joins sinks back to trials by name.
14
+
15
+ Like every module in `wmo.evals.harbor`, this module imports the harbor SDK
16
+ at module scope and is therefore imported lazily by its consumers; `import
17
+ wmo.distill` must succeed without the harbor extra.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import logging
23
+ from pathlib import Path
24
+ from typing import Literal
25
+
26
+ from harbor.models.task.config import MCPServerConfig
27
+
28
+ from wmo.core.types import JsonObject
29
+ from wmo.evals.harbor.agent import (
30
+ DEFAULT_EPISODE_WORKERS,
31
+ MAX_ENVIRONMENT_COMMAND_TIMEOUT_SEC,
32
+ WmoHarborAgent,
33
+ )
34
+ from wmo.harness.runtime import DEFAULT_EVAL_EPISODE_TIMEOUT_S
35
+ from wmo.providers.base import Provider, ProviderConfig, ProviderKind
36
+ from wmo.providers.retry import wrap_provider_with_retries
37
+ from wmo.providers.tinker import TinkerChatProvider, TokenRecorder
38
+
39
+ WMO_DISTILL_HARBOR_AGENT_IMPORT_PATH = "wmo.distill.agents:WmoDistillHarborAgent"
40
+
41
+
42
+ class WmoDistillHarborAgent(WmoHarborAgent):
43
+ """Runs the candidate with a Tinker student provider that records token spans.
44
+
45
+ Everything else (episode execution, trace persistence, cancellation
46
+ semantics) is inherited from `WmoHarborAgent` unchanged.
47
+
48
+ Args:
49
+ token_sink_dir: Directory the per-trial span sink is written into; the
50
+ sink file is named `{trial_name}.jsonl` where the trial name is
51
+ derived from harbor's logs-dir layout (`{trial_dir}/agent`).
52
+ """
53
+
54
+ token_sink_path: Path
55
+ """The exact sink file this trial's recorder writes (set at construction)."""
56
+
57
+ def __init__(
58
+ self,
59
+ logs_dir: Path,
60
+ model_name: str | None = None,
61
+ logger: logging.Logger | None = None,
62
+ mcp_servers: list[MCPServerConfig] | None = None,
63
+ skills_dir: str | None = None,
64
+ *,
65
+ token_sink_dir: str,
66
+ command_timeout_sec: int = MAX_ENVIRONMENT_COMMAND_TIMEOUT_SEC,
67
+ extra_env: dict[str, str] | None = None,
68
+ harness: JsonObject,
69
+ provider_config: JsonObject,
70
+ harness_backend: Literal["local", "e2b"] = "local",
71
+ e2b_template: str | None = None,
72
+ episode_timeout_sec: float = DEFAULT_EVAL_EPISODE_TIMEOUT_S,
73
+ episode_workers: int = DEFAULT_EPISODE_WORKERS,
74
+ context_window: int | None = None,
75
+ ) -> None:
76
+ if not isinstance(token_sink_dir, str) or not token_sink_dir:
77
+ raise ValueError(
78
+ "token_sink_dir must be a nonempty path string; the distill rollout "
79
+ "collector passes it through the scorer's extra_agent_kwargs"
80
+ )
81
+ # Set before super().__init__: the base constructor calls _build_provider,
82
+ # which reads this (and the BaseAgent-assigned logs_dir).
83
+ self._token_sink_dir = Path(token_sink_dir)
84
+ super().__init__(
85
+ logs_dir=logs_dir,
86
+ model_name=model_name,
87
+ logger=logger,
88
+ mcp_servers=mcp_servers,
89
+ skills_dir=skills_dir,
90
+ command_timeout_sec=command_timeout_sec,
91
+ extra_env=extra_env,
92
+ harness=harness,
93
+ provider_config=provider_config,
94
+ harness_backend=harness_backend,
95
+ e2b_template=e2b_template,
96
+ episode_timeout_sec=episode_timeout_sec,
97
+ episode_workers=episode_workers,
98
+ context_window=context_window,
99
+ )
100
+
101
+ def _build_provider(self, config: ProviderConfig) -> Provider:
102
+ """Build the retry-wrapped Tinker student provider with this trial's span sink.
103
+
104
+ Args:
105
+ config: The validated worker provider config; must be the tinker kind
106
+ (distillation trains on the student's exact sampled tokens, which
107
+ only the Tinker provider records).
108
+
109
+ Returns:
110
+ The retry-wrapped `TinkerChatProvider`. Spans record only after a
111
+ completion fully succeeds, so retries never duplicate them.
112
+
113
+ Raises:
114
+ ValueError: If the provider kind is not tinker, or the trial name
115
+ cannot be derived from the logs dir.
116
+ """
117
+ if config.kind is not ProviderKind.TINKER:
118
+ raise ValueError(
119
+ "distillation rollouts must sample the Tinker student so its token "
120
+ f"spans can be recorded, got provider kind {config.kind.value!r}; "
121
+ "configure the worker provider with kind 'tinker' (or run the standard "
122
+ "WmoHarborAgent when no token capture is wanted)"
123
+ )
124
+ trial_name = self.logs_dir.parent.name
125
+ if not trial_name:
126
+ raise ValueError(
127
+ f"cannot derive the harbor trial name from logs_dir {self.logs_dir}; "
128
+ "the distill agent expects harbor's per-trial {trial_dir}/agent layout"
129
+ )
130
+ self._token_sink_dir.mkdir(parents=True, exist_ok=True)
131
+ sink_path = self._token_sink_dir / f"{trial_name}.jsonl"
132
+ # The recorder appends and call_index restarts at 0 per recorder; a leftover
133
+ # sink from a pruned earlier attempt of the same trial name would corrupt the
134
+ # contiguous call_index sequence load_trial_spans enforces.
135
+ sink_path.unlink(missing_ok=True)
136
+ self.token_sink_path = sink_path
137
+ provider = TinkerChatProvider(config, recorder=TokenRecorder(jsonl_path=sink_path))
138
+ # Same retry contract as the base bridge: one transient capacity error must
139
+ # not kill a whole trial.
140
+ return wrap_provider_with_retries(provider)