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/mutate.py ADDED
@@ -0,0 +1,343 @@
1
+ """The delta proposer: the meta-agent reads a harness document and emits one `HarnessDelta`.
2
+
3
+ The proposer is shown the parent's surfaces — each with its id, kind, content hash, and content —
4
+ plus the failure evidence for one clustered mechanism, and replies with the delta's ops,
5
+ preconditions, and expected effect as JSON. The trigger, lineage, and identity fields are filled by
6
+ the caller from ground truth (the cluster and the parent's hashes), never trusted from the model.
7
+
8
+ Preconditions are copy-not-guess: the prompt prints every surface's current hash, and the proposer
9
+ must echo the hash of each surface it replaces or removes. `apply_delta` then rejects atomically on
10
+ any mismatch, so a proposal is only ever applied to exactly the document it was drafted against.
11
+
12
+ An unusable reply (no JSON, wrong shape) returns None — the search counts it as a skipped
13
+ iteration; a flaky meta-model costs budget, not the run.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+
20
+ from pydantic import BaseModel, Field, ValidationError
21
+
22
+ from wmo.core.parsing import extract_json_object
23
+ from wmo.core.text import normalize_durable_text
24
+ from wmo.evals.closed_loop import ClosedLoopReport, RolloutEvidence
25
+ from wmo.evals.gold import GoldVerdict
26
+ from wmo.evals.tasks import TaskSpec
27
+ from wmo.harness.delta import FailureSignature, HarnessDelta, SurfaceOp, compute_delta_id
28
+ from wmo.harness.doc import HarnessDoc
29
+ from wmo.harness.tools import SUBMIT, TOOL_REGISTRY
30
+ from wmo.providers.base import Message, Provider
31
+
32
+ _TRACE_CHARS_PER_ATTEMPT = 12_000
33
+
34
+ MUTATE_SYSTEM = f"""You are a meta-agent improving an agent harness — a document of named \
35
+ SURFACES that configure an agent. You do NOT solve the agent's tasks yourself. You are shown the \
36
+ harness's current surfaces (each with its id, kind, and content hash) and evidence of ONE failure \
37
+ mechanism its agent exhibits. Propose exactly ONE focused change, grounded in that evidence. Do \
38
+ not shotgun many unrelated changes at once.
39
+
40
+ Choose the lever that can actually EXPRESS the fix:
41
+ - Structural/behavioral mechanisms (the agent claims without acting, loses context, wastes turns,
42
+ never verifies, mishandles errors) -> edit the agent's CODE. For an in-process harness that is
43
+ the singleton `code:runtime` program; for a real multi-file harness (e.g. the vendored pi
44
+ agent) it is the pathful `code:<...>` surface that owns the behavior — the agent's actual
45
+ source, shown to you above with its path. Edit the file where the mechanism lives (the turn
46
+ loop, context compaction, tool dispatch, answer checking). This is the strongest lever:
47
+ loops, verification passes, retries, observation truncation, and compaction are all code.
48
+ - A missing technique on specific tasks -> ADD a `skill:<slug>` teaching it.
49
+ - The agent misusing, missing, or not needing a tool -> edit `tool_policy:main`.
50
+ - Erratic sampling or turn caps -> adjust a `param:*`.
51
+ - Replace a `prompt:*` surface only for wording-level problems; prompt rewrites are the weakest
52
+ lever and most often regress tasks that currently pass.
53
+
54
+ The `code:runtime` contract — a Python module defining `run(kit) -> str` (the final answer):
55
+ - `kit.instruction` (the task), `kit.system_prompt` (the assembled prompt), `kit.task_id`.
56
+ - `kit.complete(system, messages, temperature=..., max_tokens=...) -> str` — one LLM call;
57
+ messages are `("user"|"assistant", content)` tuples. BUDGETED (default 40 calls/episode).
58
+ - `kit.execute(tool, arguments) -> Observation` (`.content`, `.is_error`) — one environment
59
+ action, validated against the tool policy. BUDGETED (default 40/episode). Every call is
60
+ recorded to the transcript the judge scores; work not done through `kit.execute` does not
61
+ exist as far as grading is concerned.
62
+ - `kit.parse_tool_call(text) -> ToolCall | None` (`.tool`, `.arguments`), `kit.tools_text()`,
63
+ `kit.skills_index()`, `kit.read_skill(name)`.
64
+ - Exceptions and exhausted budgets fail the episode (partial transcript kept). The module must
65
+ be self-contained (stdlib imports only) and deterministic apart from LLM calls.
66
+
67
+ Surface kinds:
68
+ - prompt — a section of the agent's system prompt (all prompt surfaces are joined in id order).
69
+ - tool_policy — the tool list, one tool name per line. Valid names: \
70
+ {", ".join(sorted(TOOL_REGISTRY))}. The `{SUBMIT.name}` tool is REQUIRED (without it a run \
71
+ cannot end).
72
+ - param — a scalar loop knob: `param:max-turns` (int >= 1), `param:max-output-tokens` for a
73
+ `pi-node` harness (int >= 1), or `param:temperature` (float in [0, 2]).
74
+ - code — the agent's source. Either the singleton in-process `code:runtime` (contract above; must
75
+ compile and define `run`), OR the pathful `code:<...>` files of a real multi-file harness (the
76
+ vendored pi agent's own source). Editing a pathful `code:` surface REPLACES that file's whole
77
+ content; keep it a valid module and change only what the fix needs — you are editing the real
78
+ harness that runs, not a description of it.
79
+ - skill — one reusable technique, shaped as:
80
+ ---
81
+ name: <kebab-slug matching the surface id>
82
+ description: <one-line trigger description>
83
+ ---
84
+ <skill body markdown>
85
+
86
+ Reply with ONLY a JSON object, no prose:
87
+ {{"expected_effect": "<falsifiable prediction: what should change if this works>",
88
+ "preconditions": {{"<surface_id>": "<that surface's content hash, copied from above>"}},
89
+ "ops": [{{"op": "add" | "replace" | "remove",
90
+ "surface_id": "<kind>:<kebab-slug>",
91
+ "kind": "<required on add>",
92
+ "content": "<the FULL new content (omit on remove)>",
93
+ "rationale": "<why this op should help>"}}]}}
94
+
95
+ Rules:
96
+ - Every surface you replace or remove MUST appear in `preconditions` with its hash copied verbatim.
97
+ - `content` is the complete new surface content, not a diff.
98
+ - An `add` uses a fresh surface id of the right kind (e.g. a new `skill:<slug>` or a new
99
+ `prompt:<slug>` section)."""
100
+
101
+
102
+ class _RawDelta(BaseModel):
103
+ """What the meta-agent is trusted to provide — everything else is filled from ground truth."""
104
+
105
+ expected_effect: str
106
+ preconditions: dict[str, str] = Field(default_factory=dict)
107
+ ops: list[SurfaceOp] = Field(min_length=1)
108
+
109
+
110
+ def propose_delta(
111
+ parent: HarnessDoc,
112
+ trigger: FailureSignature,
113
+ evidence: str,
114
+ provider: Provider,
115
+ *,
116
+ history: list[HarnessDelta] | None = None,
117
+ ) -> HarnessDelta | None:
118
+ """Ask the meta-agent for one delta against `parent`, or None if the reply is unusable.
119
+
120
+ `history` is the run's previously judged deltas (most recent last): their ops and gate
121
+ verdicts are shown to the proposer so it iterates instead of re-proposing rejected ideas.
122
+ """
123
+ user = _build_prompt(parent, trigger, evidence, history or [])
124
+ # The reply must hold a COMPLETE replacement surface (ops carry full content, not diffs).
125
+ # The largest vendored pi source file is ~36 KB (~10k tokens before JSON escaping), so 4k
126
+ # silently truncated every real code-surface proposal into an unusable reply; the search
127
+ # "ran" its iterations but skipped them all. 16k fits any single-surface rewrite with room
128
+ # for preconditions/rationale, and stays under common provider output caps.
129
+ completion = provider.complete(
130
+ MUTATE_SYSTEM,
131
+ [Message(role="user", content=user)],
132
+ temperature=0.9,
133
+ max_tokens=16384,
134
+ )
135
+ return parse_delta(parent, trigger, completion.text)
136
+
137
+
138
+ def parse_delta(
139
+ parent: HarnessDoc,
140
+ trigger: FailureSignature,
141
+ text: str,
142
+ ) -> HarnessDelta | None:
143
+ """Parse one full-content or compact-edit proposal against ``parent``."""
144
+ raw = extract_json_object(text)
145
+ if raw is None:
146
+ return None
147
+ try:
148
+ value = json.loads(raw)
149
+ if not isinstance(value, dict):
150
+ return None
151
+ value["ops"] = _expand_compact_ops(parent, value.get("ops"))
152
+ proposed = _RawDelta.model_validate(value)
153
+ return HarnessDelta(
154
+ delta_id=compute_delta_id(parent.doc_hash, proposed.ops),
155
+ parent_doc_hash=parent.doc_hash,
156
+ trigger=trigger,
157
+ preconditions=proposed.preconditions,
158
+ ops=proposed.ops,
159
+ expected_effect=proposed.expected_effect,
160
+ )
161
+ except (TypeError, ValueError, ValidationError):
162
+ return None
163
+
164
+
165
+ def _expand_compact_ops(parent: HarnessDoc, value: object) -> list[dict[str, object]]:
166
+ """Expand exact replacement hunks into ordinary full-content surface ops."""
167
+ if not isinstance(value, list):
168
+ raise ValueError("ops must be an array")
169
+ expanded: list[dict[str, object]] = []
170
+ for item in value:
171
+ if not isinstance(item, dict):
172
+ raise ValueError("each op must be an object")
173
+ op = dict(item)
174
+ edits = op.pop("edits", None)
175
+ if op.get("op") == "replace" and "content" not in op and edits is not None:
176
+ surface_id = op.get("surface_id")
177
+ if not isinstance(surface_id, str) or (surface := parent.surface(surface_id)) is None:
178
+ raise ValueError("compact replace targets an unknown surface")
179
+ op["content"] = _apply_edits(surface.content, edits)
180
+ elif edits is not None:
181
+ raise ValueError("edits are only valid on content-less replace ops")
182
+ expanded.append(op)
183
+ return expanded
184
+
185
+
186
+ def _apply_edits(content: str, value: object) -> str:
187
+ """Apply ordered exact edits, rejecting missing or ambiguous anchors."""
188
+ if not isinstance(value, list) or not value:
189
+ raise ValueError("edits must be a non-empty array")
190
+ for item in value:
191
+ if not isinstance(item, dict):
192
+ raise ValueError("each edit must be an object")
193
+ old = item.get("old")
194
+ new = item.get("new")
195
+ if not isinstance(old, str) or not old or not isinstance(new, str):
196
+ raise ValueError("edits need non-empty old and string new values")
197
+ if content.count(old) != 1:
198
+ raise ValueError("edit old text must occur exactly once")
199
+ content = content.replace(old, new, 1)
200
+ return content
201
+
202
+
203
+ def render_evidence(
204
+ trigger: FailureSignature, report: ClosedLoopReport, tasks: list[TaskSpec]
205
+ ) -> str:
206
+ """Render one failure cluster (instructions + unmet assertions) as reflection fuel.
207
+
208
+ A trigger with no failing tasks (the all-pass case) gets an explicit "nothing failed" prompt
209
+ asking for a generalization/efficiency improvement — not a fake failure section that would
210
+ send the meta-agent chasing nonexistent problems.
211
+ """
212
+ if not trigger.task_ids:
213
+ return (
214
+ "The harness passed every task on every pass. There are no failures to fix. "
215
+ "Propose a change that should GENERALIZE or ECONOMIZE: a tighter, more transferable "
216
+ "prompt surface; a lower param:max-turns if runs finish early; or a reusable skill "
217
+ "distilled from what worked."
218
+ )
219
+ by_id = {task.task_id: task for task in tasks}
220
+ selected = set(trigger.task_ids)
221
+ scorecard = [
222
+ "## Evaluation scorecard",
223
+ "The selected failure is marked TARGET; preserve behavior on the other tasks.",
224
+ ]
225
+ for task in tasks:
226
+ outcome = report.per_task.get(task.task_id)
227
+ success = outcome.success_rate if outcome is not None else 0.0
228
+ fraction = outcome.mean_fraction if outcome is not None else 0.0
229
+ instruction = " ".join(normalize_durable_text(task.instruction).split())
230
+ if len(instruction) > 240:
231
+ instruction = f"{instruction[:237]}..."
232
+ marker = "TARGET" if task.task_id in selected else "other"
233
+ scorecard.append(
234
+ f"- [{marker}] {task.task_id}: success={success:.2f}, "
235
+ f"assertion_fraction={fraction:.2f} — {instruction}"
236
+ )
237
+ sections = [
238
+ "\n".join(scorecard),
239
+ f"## Selected failure\n\nFailure mechanism: {trigger.mechanism}",
240
+ ]
241
+ for task_id in trigger.task_ids:
242
+ task = by_id.get(task_id)
243
+ outcome = report.per_task.get(task_id)
244
+ instruction = (
245
+ normalize_durable_text(task.instruction) if task is not None else "(unknown task)"
246
+ )
247
+ rate = f"{outcome.success_rate:.2f} over {outcome.passes} passes" if outcome else "?"
248
+ fraction = f", assertion_fraction={outcome.mean_fraction:.2f}" if outcome else ""
249
+ task_section = [
250
+ f"### Task {task_id} (success_rate={rate}{fraction})",
251
+ f"Instruction: {instruction}",
252
+ ]
253
+ if outcome is not None:
254
+ for index, verdict in enumerate(outcome.verdicts, 1):
255
+ attempt = outcome.attempts[index - 1] if index <= len(outcome.attempts) else None
256
+ task_section.append(_render_attempt(index=index, attempt=attempt, verdict=verdict))
257
+ sections.append("\n\n".join(task_section))
258
+ unmet = "\n".join(f"- {a}" for a in trigger.unmet_assertions)
259
+ sections.append(
260
+ "Original trigger assertions from the parent (current attempt verdicts above are "
261
+ f"authoritative):\n{unmet or '- (none recorded)'}"
262
+ )
263
+ return "\n\n".join(sections)
264
+
265
+
266
+ def _render_attempt(*, index: int, attempt: RolloutEvidence | None, verdict: GoldVerdict) -> str:
267
+ """Render one rollout plus its judge feedback without unbounded prompt growth."""
268
+ # Keep this helper tolerant of old/deserialized reports that predate RolloutEvidence. The
269
+ # verdict fields remain useful even when no trace was retained.
270
+ lines = [
271
+ f"#### Attempt {index} (passed={str(verdict.passed).lower()}, "
272
+ f"assertion_fraction={verdict.fraction:.2f})"
273
+ ]
274
+ if attempt is not None:
275
+ lines.append(
276
+ f"Stop: {attempt.stop_reason.value}; turns={attempt.turns}\n"
277
+ f"Final answer:\n{normalize_durable_text(attempt.answer) or '(none)'}\n\n"
278
+ f"Execution transcript:\n"
279
+ f"{_bounded_trace(normalize_durable_text(attempt.transcript))}"
280
+ )
281
+ if verdict.assertions:
282
+ judged = "\n".join(
283
+ f"- {'PASS' if assertion.passed else 'FAIL'}: "
284
+ f"{normalize_durable_text(assertion.assertion)}"
285
+ f" — {normalize_durable_text(assertion.why) if assertion.why else '(no judge reason)'}"
286
+ for assertion in verdict.assertions
287
+ )
288
+ lines.append(f"Judge feedback:\n{judged}")
289
+ else:
290
+ rationale = normalize_durable_text(verdict.rationale) if verdict.rationale else ""
291
+ lines.append(f"Judge feedback: {rationale or '(no per-assertion feedback)'}")
292
+ return "\n\n".join(lines)
293
+
294
+
295
+ def _bounded_trace(trace: str, limit: int = _TRACE_CHARS_PER_ATTEMPT) -> str:
296
+ """Keep both the setup and terminal behavior when a long run exceeds the evidence budget."""
297
+ if not trace:
298
+ return "(empty)"
299
+ if len(trace) <= limit:
300
+ return trace
301
+ head = limit // 2
302
+ tail = limit - head
303
+ omitted = len(trace) - limit
304
+ return f"{trace[:head]}\n... ({omitted} trace characters omitted) ...\n{trace[-tail:]}"
305
+
306
+
307
+ def render_history(history: list[HarnessDelta], limit: int = 5) -> str:
308
+ """The last `limit` judged deltas as lessons: what was tried, and exactly how it fared."""
309
+ lines: list[str] = []
310
+ for delta in history[-limit:]:
311
+ ops = ", ".join(f"{op.op} {op.surface_id}" for op in delta.ops)
312
+ verdict = delta.verdict.reason if delta.verdict is not None else "(never evaluated)"
313
+ rationale = delta.ops[0].rationale if delta.ops else ""
314
+ lines.append(f"- [{ops}] rationale: {rationale[:120]}\n outcome: {verdict}")
315
+ return "\n".join(lines)
316
+
317
+
318
+ def _build_prompt(
319
+ parent: HarnessDoc, trigger: FailureSignature, evidence: str, history: list[HarnessDelta]
320
+ ) -> str:
321
+ """The meta-agent's user prompt: every parent surface with its identity, then the evidence."""
322
+ blocks: list[str] = []
323
+ for surface in parent.surfaces:
324
+ budget = f", budget={surface.budget}" if surface.budget is not None else ""
325
+ blocks.append(
326
+ f"### {surface.id} (kind={surface.kind.value}, "
327
+ f"hash={surface.content_hash}{budget})\n```\n{surface.content}\n```"
328
+ )
329
+ surfaces_block = "\n\n".join(blocks)
330
+ history_block = (
331
+ f"## Previous attempts this run (do NOT repeat failed ideas — iterate or change lever)"
332
+ f"\n\n{render_history(history)}\n\n"
333
+ if history
334
+ else ""
335
+ )
336
+ return (
337
+ f"## Current harness surfaces ({parent.name}, doc_hash={parent.doc_hash})\n\n"
338
+ f"{surfaces_block}\n\n"
339
+ f"## Failure evidence\n\n{evidence}\n\n"
340
+ f"{history_block}"
341
+ "Propose ONE focused change as the JSON object described in your instructions. "
342
+ "Remember: copy the hash of every surface you replace or remove into `preconditions`."
343
+ )