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.
- llm_waterfall/LICENSE +21 -0
- llm_waterfall/__init__.py +53 -0
- llm_waterfall/adapters/__init__.py +36 -0
- llm_waterfall/adapters/anthropic.py +105 -0
- llm_waterfall/adapters/aws_mantle.py +47 -0
- llm_waterfall/adapters/azure_openai.py +71 -0
- llm_waterfall/adapters/base.py +51 -0
- llm_waterfall/adapters/bedrock.py +309 -0
- llm_waterfall/adapters/openai.py +130 -0
- llm_waterfall/classify.py +184 -0
- llm_waterfall/pricing.py +110 -0
- llm_waterfall/py.typed +0 -0
- llm_waterfall/types.py +295 -0
- llm_waterfall/waterfall.py +255 -0
- wmo/__init__.py +38 -0
- wmo/agents/__init__.py +7 -0
- wmo/agents/default.py +29 -0
- wmo/agents/meta.py +55 -0
- wmo/agents/optimizer.py +55 -0
- wmo/agents/project.py +928 -0
- wmo/cli/__init__.py +5 -0
- wmo/cli/agent_session.py +1123 -0
- wmo/cli/app.py +2489 -0
- wmo/cli/e2b_cmds.py +212 -0
- wmo/cli/eval_closed_loop.py +207 -0
- wmo/cli/harness_app.py +1147 -0
- wmo/cli/harness_distill.py +659 -0
- wmo/cli/hosted_session.py +880 -0
- wmo/cli/ingest_cmd.py +165 -0
- wmo/cli/model_roles.py +82 -0
- wmo/cli/platform_cmds.py +372 -0
- wmo/cli/route_app.py +274 -0
- wmo/cli/session_state.py +243 -0
- wmo/cli/ui.py +1107 -0
- wmo/cli/workspace_sync.py +504 -0
- wmo/config/__init__.py +60 -0
- wmo/config/card.py +129 -0
- wmo/config/config.py +367 -0
- wmo/config/dotenv.py +67 -0
- wmo/config/settings.py +128 -0
- wmo/config/store.py +177 -0
- wmo/conftest.py +19 -0
- wmo/connect/__init__.py +88 -0
- wmo/connect/apps.py +78 -0
- wmo/connect/brave.py +284 -0
- wmo/connect/connector.py +79 -0
- wmo/connect/credentials.py +164 -0
- wmo/connect/github.py +321 -0
- wmo/connect/google.py +627 -0
- wmo/connect/notion.py +790 -0
- wmo/connect/oauth.py +461 -0
- wmo/connect/slack.py +555 -0
- wmo/connect/store.py +199 -0
- wmo/connect/types.py +156 -0
- wmo/core/__init__.py +21 -0
- wmo/core/parsing.py +281 -0
- wmo/core/render.py +271 -0
- wmo/core/text.py +40 -0
- wmo/core/types.py +116 -0
- wmo/distill/__init__.py +14 -0
- wmo/distill/agents.py +140 -0
- wmo/distill/config.py +1006 -0
- wmo/distill/cost.py +437 -0
- wmo/distill/data.py +921 -0
- wmo/distill/deadlines.py +254 -0
- wmo/distill/fake_tinker.py +734 -0
- wmo/distill/gate.py +122 -0
- wmo/distill/loop.py +3499 -0
- wmo/distill/renderers.py +399 -0
- wmo/distill/rendering.py +620 -0
- wmo/distill/rollouts.py +726 -0
- wmo/distill/samples.py +195 -0
- wmo/distill/store.py +829 -0
- wmo/distill/teacher.py +714 -0
- wmo/distill/tokens.py +535 -0
- wmo/distill/tracking.py +552 -0
- wmo/distill/tripwire.py +411 -0
- wmo/distill/xtoken/byte_offsets.py +152 -0
- wmo/distill/xtoken/chunks.py +457 -0
- wmo/distill/xtoken/prompt_logprobs.py +475 -0
- wmo/distill/xtoken/teacher_render.py +346 -0
- wmo/engine/__init__.py +28 -0
- wmo/engine/autoconfig.py +367 -0
- wmo/engine/build.py +346 -0
- wmo/engine/demo.py +77 -0
- wmo/engine/eval_suites.py +245 -0
- wmo/engine/grounding.py +491 -0
- wmo/engine/knowledge.py +291 -0
- wmo/engine/loader.py +36 -0
- wmo/engine/play.py +92 -0
- wmo/engine/prompts.py +99 -0
- wmo/engine/replay.py +443 -0
- wmo/engine/reporting.py +58 -0
- wmo/engine/workspace.py +468 -0
- wmo/engine/world_model.py +568 -0
- wmo/env/__init__.py +22 -0
- wmo/env/base.py +121 -0
- wmo/env/closed_loop.py +229 -0
- wmo/env/episode.py +107 -0
- wmo/env/llm_agent.py +93 -0
- wmo/env/scenarios.py +73 -0
- wmo/evals/__init__.py +52 -0
- wmo/evals/agreement.py +110 -0
- wmo/evals/base.py +45 -0
- wmo/evals/closed_loop.py +480 -0
- wmo/evals/failover.py +96 -0
- wmo/evals/gold.py +127 -0
- wmo/evals/grid.py +394 -0
- wmo/evals/grid_plot.py +205 -0
- wmo/evals/harbor/__init__.py +27 -0
- wmo/evals/harbor/agent.py +573 -0
- wmo/evals/harbor/ctrf.py +171 -0
- wmo/evals/harbor/e2b_environment.py +587 -0
- wmo/evals/harbor/e2b_template_policy.py +144 -0
- wmo/evals/harbor/scorer.py +875 -0
- wmo/evals/harbor/tasks.py +140 -0
- wmo/evals/open_loop.py +194 -0
- wmo/evals/tasks.py +53 -0
- wmo/harness/__init__.py +51 -0
- wmo/harness/code_runtime.py +288 -0
- wmo/harness/create.py +1191 -0
- wmo/harness/delta.py +220 -0
- wmo/harness/doc.py +556 -0
- wmo/harness/e2b_ledger.py +342 -0
- wmo/harness/e2b_reap.py +476 -0
- wmo/harness/e2b_sandbox.py +350 -0
- wmo/harness/environment.py +35 -0
- wmo/harness/live_session.py +543 -0
- wmo/harness/mutate.py +343 -0
- wmo/harness/pi_e2b.py +1710 -0
- wmo/harness/pi_entry/entry.ts +268 -0
- wmo/harness/pi_entry/runner_frames.ts +92 -0
- wmo/harness/pi_entry/runner_live.ts +587 -0
- wmo/harness/pi_entry/runner_service.ts +270 -0
- wmo/harness/pi_entry/runner_stdio.ts +374 -0
- wmo/harness/pi_entry/runner_termination.ts +142 -0
- wmo/harness/pi_local.py +262 -0
- wmo/harness/pi_runtime.py +495 -0
- wmo/harness/pi_vendor.py +65 -0
- wmo/harness/population.py +509 -0
- wmo/harness/project_proposer.py +569 -0
- wmo/harness/proposer.py +977 -0
- wmo/harness/runner_link.py +619 -0
- wmo/harness/runtime.py +389 -0
- wmo/harness/scoring.py +247 -0
- wmo/harness/skills.py +116 -0
- wmo/harness/source_tree.py +319 -0
- wmo/harness/store.py +176 -0
- wmo/harness/tools.py +105 -0
- wmo/harness/vendor/manifest.sha256 +58 -0
- wmo/harness/vendor/pi-agent/CHANGELOG.md +556 -0
- wmo/harness/vendor/pi-agent/LICENSE +21 -0
- wmo/harness/vendor/pi-agent/README.md +488 -0
- wmo/harness/vendor/pi-agent/VENDOR.md +39 -0
- wmo/harness/vendor/pi-agent/docs/agent-harness.md +486 -0
- wmo/harness/vendor/pi-agent/docs/durable-harness.md +212 -0
- wmo/harness/vendor/pi-agent/docs/hooks.md +445 -0
- wmo/harness/vendor/pi-agent/docs/models.md +966 -0
- wmo/harness/vendor/pi-agent/docs/observability.md +376 -0
- wmo/harness/vendor/pi-agent/package.json +60 -0
- wmo/harness/vendor/pi-agent/src/agent-loop.ts +748 -0
- wmo/harness/vendor/pi-agent/src/agent.ts +575 -0
- wmo/harness/vendor/pi-agent/src/harness/agent-harness.ts +1029 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/branch-summarization.ts +261 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/compaction.ts +747 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/utils.ts +144 -0
- wmo/harness/vendor/pi-agent/src/harness/env/nodejs.ts +550 -0
- wmo/harness/vendor/pi-agent/src/harness/messages.ts +164 -0
- wmo/harness/vendor/pi-agent/src/harness/prompt-templates.ts +267 -0
- wmo/harness/vendor/pi-agent/src/harness/session/jsonl-repo.ts +177 -0
- wmo/harness/vendor/pi-agent/src/harness/session/jsonl-storage.ts +293 -0
- wmo/harness/vendor/pi-agent/src/harness/session/memory-repo.ts +50 -0
- wmo/harness/vendor/pi-agent/src/harness/session/memory-storage.ts +131 -0
- wmo/harness/vendor/pi-agent/src/harness/session/repo-utils.ts +51 -0
- wmo/harness/vendor/pi-agent/src/harness/session/session.ts +267 -0
- wmo/harness/vendor/pi-agent/src/harness/session/uuid.ts +54 -0
- wmo/harness/vendor/pi-agent/src/harness/skills.ts +375 -0
- wmo/harness/vendor/pi-agent/src/harness/system-prompt.ts +34 -0
- wmo/harness/vendor/pi-agent/src/harness/types.ts +836 -0
- wmo/harness/vendor/pi-agent/src/harness/utils/shell-output.ts +135 -0
- wmo/harness/vendor/pi-agent/src/harness/utils/truncate.ts +344 -0
- wmo/harness/vendor/pi-agent/src/index.ts +44 -0
- wmo/harness/vendor/pi-agent/src/node.ts +2 -0
- wmo/harness/vendor/pi-agent/src/proxy.ts +367 -0
- wmo/harness/vendor/pi-agent/src/types.ts +428 -0
- wmo/harness/vendor/pi-agent/test/agent-loop.test.ts +1351 -0
- wmo/harness/vendor/pi-agent/test/agent.test.ts +699 -0
- wmo/harness/vendor/pi-agent/test/e2e.test.ts +404 -0
- wmo/harness/vendor/pi-agent/test/harness/agent-harness-stream.test.ts +213 -0
- wmo/harness/vendor/pi-agent/test/harness/agent-harness.test.ts +608 -0
- wmo/harness/vendor/pi-agent/test/harness/compaction.test.ts +655 -0
- wmo/harness/vendor/pi-agent/test/harness/nodejs-env.test.ts +321 -0
- wmo/harness/vendor/pi-agent/test/harness/prompt-templates.test.ts +90 -0
- wmo/harness/vendor/pi-agent/test/harness/repo.test.ts +68 -0
- wmo/harness/vendor/pi-agent/test/harness/resource-formatting.test.ts +24 -0
- wmo/harness/vendor/pi-agent/test/harness/session-test-utils.ts +55 -0
- wmo/harness/vendor/pi-agent/test/harness/session-uuid.test.ts +50 -0
- wmo/harness/vendor/pi-agent/test/harness/session.test.ts +156 -0
- wmo/harness/vendor/pi-agent/test/harness/skills.test.ts +116 -0
- wmo/harness/vendor/pi-agent/test/harness/storage.test.ts +299 -0
- wmo/harness/vendor/pi-agent/test/harness/system-prompt.test.ts +66 -0
- wmo/harness/vendor/pi-agent/test/harness/truncate.test.ts +169 -0
- wmo/harness/vendor/pi-agent/test/scratch/simple.ts +72 -0
- wmo/harness/vendor/pi-agent/test/utils/calculate.ts +32 -0
- wmo/harness/vendor/pi-agent/test/utils/get-current-time.ts +46 -0
- wmo/harness/vendor/pi-agent/tsconfig.build.json +13 -0
- wmo/harness/vendor/pi-agent/vitest.config.ts +19 -0
- wmo/harness/vendor/pi-agent/vitest.harness.config.ts +28 -0
- wmo/harness/vendor/vendor_pi.sh +59 -0
- wmo/harness/workspace_patch.py +270 -0
- wmo/ingest/__init__.py +47 -0
- wmo/ingest/adapter.py +72 -0
- wmo/ingest/base.py +114 -0
- wmo/ingest/braintrust.py +339 -0
- wmo/ingest/detect.py +126 -0
- wmo/ingest/langfuse.py +291 -0
- wmo/ingest/langsmith.py +444 -0
- wmo/ingest/mastra.py +330 -0
- wmo/ingest/messages.py +170 -0
- wmo/ingest/normalize.py +679 -0
- wmo/ingest/otel_genai.py +69 -0
- wmo/ingest/otel_writer.py +100 -0
- wmo/ingest/phoenix.py +150 -0
- wmo/ingest/postgres.py +246 -0
- wmo/ingest/posthog.py +320 -0
- wmo/ingest/quality.py +28 -0
- wmo/ingest/stream.py +209 -0
- wmo/ingest/testdata/sample_otlp.json +60 -0
- wmo/ingest/testdata/sample_spans.jsonl +3 -0
- wmo/optimize/__init__.py +25 -0
- wmo/optimize/base.py +143 -0
- wmo/optimize/gepa.py +806 -0
- wmo/optimize/judge.py +262 -0
- wmo/optimize/judge_quality.py +359 -0
- wmo/optimize/knn.py +468 -0
- wmo/optimize/numeric.py +152 -0
- wmo/optimize/outcomes.py +103 -0
- wmo/optimize/policy.py +669 -0
- wmo/optimize/report.py +231 -0
- wmo/optimize/reward.py +129 -0
- wmo/optimize/routing.py +373 -0
- wmo/platform/__init__.py +6 -0
- wmo/platform/auth.py +115 -0
- wmo/platform/client.py +551 -0
- wmo/platform/credentials.py +126 -0
- wmo/platform/transfer.py +158 -0
- wmo/providers/__init__.py +40 -0
- wmo/providers/_bedrock_chat.py +155 -0
- wmo/providers/_openai_common.py +182 -0
- wmo/providers/_responses_common.py +472 -0
- wmo/providers/anthropic.py +134 -0
- wmo/providers/azure_openai.py +296 -0
- wmo/providers/base.py +300 -0
- wmo/providers/bedrock.py +312 -0
- wmo/providers/models.py +205 -0
- wmo/providers/openai.py +143 -0
- wmo/providers/openai_responses.py +240 -0
- wmo/providers/pool.py +170 -0
- wmo/providers/registry.py +73 -0
- wmo/providers/retry.py +151 -0
- wmo/providers/tinker.py +936 -0
- wmo/providers/waterfall.py +336 -0
- wmo/research/__init__.py +81 -0
- wmo/research/ablation.py +133 -0
- wmo/research/concurrency_plot.py +523 -0
- wmo/research/concurrency_run.py +240 -0
- wmo/research/concurrency_scaling.py +270 -0
- wmo/research/gepa_scaling.py +274 -0
- wmo/research/pipeline.py +198 -0
- wmo/research/scaling_split.py +82 -0
- wmo/research/scenario_fidelity.py +198 -0
- wmo/research/scenario_recovery.py +92 -0
- wmo/research/seed_stability.py +90 -0
- wmo/research/trace_scaling.py +348 -0
- wmo/retrieval/__init__.py +6 -0
- wmo/retrieval/embedders.py +105 -0
- wmo/retrieval/leakfree.py +52 -0
- wmo/retrieval/retriever.py +173 -0
- wmo/scenarios/__init__.py +58 -0
- wmo/scenarios/builder.py +152 -0
- wmo/scenarios/mining/__init__.py +27 -0
- wmo/scenarios/mining/clustering.py +171 -0
- wmo/scenarios/mining/facets.py +226 -0
- wmo/scenarios/mining/selection.py +220 -0
- wmo/scenarios/synthesis/__init__.py +6 -0
- wmo/scenarios/synthesis/scenario_set.py +63 -0
- wmo/scenarios/synthesis/synthesizer.py +85 -0
- wmo/scenarios/verification/__init__.py +17 -0
- wmo/scenarios/verification/judge.py +97 -0
- wmo/scenarios/verification/verify.py +135 -0
- wmo/serving/__init__.py +5 -0
- wmo/serving/builds.py +451 -0
- wmo/serving/chat.py +878 -0
- wmo/serving/endpoint_config.py +64 -0
- wmo/serving/savings.py +250 -0
- wmo/serving/server.py +553 -0
- wmo/serving/traces_source.py +206 -0
- wmo/telemetry.py +213 -0
- wmo/tracking/__init__.py +36 -0
- wmo/tracking/clock.py +24 -0
- wmo/tracking/metered.py +125 -0
- wmo/tracking/pricing.py +99 -0
- wmo/tracking/store.py +31 -0
- wmo/tracking/tracker.py +149 -0
- world_model_optimizer-0.2.0.dist-info/METADATA +203 -0
- world_model_optimizer-0.2.0.dist-info/RECORD +308 -0
- world_model_optimizer-0.2.0.dist-info/WHEEL +4 -0
- world_model_optimizer-0.2.0.dist-info/entry_points.txt +2 -0
wmo/harness/delta.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""`HarnessDelta`: the typed update object `wmo optimize` searches through.
|
|
2
|
+
|
|
3
|
+
The update representation IS the search space: everything the meta-agent can learn about *how to
|
|
4
|
+
improve harnesses* is bounded by what the update object can express. A raw file edit expresses
|
|
5
|
+
almost nothing — no typed target, no assertion about what it believed it was editing, no per-change
|
|
6
|
+
rationale, no verdict. A delta expresses all four:
|
|
7
|
+
|
|
8
|
+
- **trigger** (`FailureSignature`): the clustered failure mechanism this delta answers to, so the
|
|
9
|
+
archive can ask "which kinds of edits work on which failure classes?" instead of "what changed?".
|
|
10
|
+
- **preconditions**: `surface_id -> expected content hash` of every surface the proposer read
|
|
11
|
+
before editing. Application is atomic — ANY mismatch rejects the whole delta before a token of
|
|
12
|
+
eval budget is spent, so a proposal drafted against one parent can never silently misapply to
|
|
13
|
+
another.
|
|
14
|
+
- **ops** (`SurfaceOp`): add/replace/remove keyed by surface *identity*, each carrying its own
|
|
15
|
+
rationale bound to the op it justifies.
|
|
16
|
+
- **verdict** (`GateRecord`): the acceptance decision and the gate deltas that produced it, written
|
|
17
|
+
back onto the delta. The archive is a lineage of audited deltas, not a pile of snapshots.
|
|
18
|
+
|
|
19
|
+
`expected_effect` makes every delta a falsifiable prediction: at gate time the trigger cluster is
|
|
20
|
+
re-checked and the outcome lands in `verdict.reason`, measuring the proposer's calibration over
|
|
21
|
+
time, not just its win rate.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import hashlib
|
|
27
|
+
from typing import Literal
|
|
28
|
+
|
|
29
|
+
from pydantic import BaseModel, Field, model_validator
|
|
30
|
+
|
|
31
|
+
from wmo.core.text import validate_durable_text
|
|
32
|
+
from wmo.harness.doc import HarnessDoc, Surface, SurfaceKind
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class FailureSignature(BaseModel):
|
|
36
|
+
"""The clustered failure mechanism a delta answers to: WHY it exists, queryably.
|
|
37
|
+
|
|
38
|
+
Built deterministically from a closed-loop report (`wmo.harness.create.cluster_failures`),
|
|
39
|
+
never free-typed by the proposer — so the archive's mechanism labels are comparable across
|
|
40
|
+
deltas and runs.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
mechanism: str # e.g. the shared unmet assertion, or "none: all tasks pass"
|
|
44
|
+
task_ids: list[str] = Field(default_factory=list) # the failing tasks exhibiting it
|
|
45
|
+
unmet_assertions: list[str] = Field(default_factory=list) # deduped, order-stable
|
|
46
|
+
|
|
47
|
+
@model_validator(mode="after")
|
|
48
|
+
def _validate_text(self) -> FailureSignature:
|
|
49
|
+
validate_durable_text(self.mechanism, field="failure mechanism")
|
|
50
|
+
for task_id in self.task_ids:
|
|
51
|
+
validate_durable_text(task_id, field="failure task id")
|
|
52
|
+
for assertion in self.unmet_assertions:
|
|
53
|
+
validate_durable_text(assertion, field="failure assertion")
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SurfaceOp(BaseModel):
|
|
58
|
+
"""One typed edit to one surface, addressed by identity, justified in place."""
|
|
59
|
+
|
|
60
|
+
op: Literal["add", "replace", "remove"]
|
|
61
|
+
surface_id: str
|
|
62
|
+
kind: SurfaceKind | None = None # required on add; on replace must match the existing kind
|
|
63
|
+
content: str | None = None # the full new content (component rewrite, never a line diff)
|
|
64
|
+
budget: int | None = Field(default=None, ge=1) # replace: None inherits the existing budget
|
|
65
|
+
# File path for multi-file CODE surfaces (e.g. vendored pi `code:src-...`): on replace, None
|
|
66
|
+
# inherits the existing surface's path (so editing pi source keeps it a valid pathful surface);
|
|
67
|
+
# required to introduce a brand-new pathful code surface.
|
|
68
|
+
path: str | None = None
|
|
69
|
+
rationale: str # why THIS op, bound to the op — not one motivation string for the whole delta
|
|
70
|
+
|
|
71
|
+
@model_validator(mode="after")
|
|
72
|
+
def _validate_shape(self) -> SurfaceOp:
|
|
73
|
+
if self.content is not None:
|
|
74
|
+
validate_durable_text(self.content, field=f"{self.surface_id!r} operation content")
|
|
75
|
+
validate_durable_text(self.rationale, field=f"{self.surface_id!r} operation rationale")
|
|
76
|
+
if self.op == "add" and self.kind is None:
|
|
77
|
+
raise ValueError(f"add of {self.surface_id!r} must declare a kind")
|
|
78
|
+
if self.op in ("add", "replace") and self.content is None:
|
|
79
|
+
raise ValueError(f"{self.op} of {self.surface_id!r} must carry content")
|
|
80
|
+
if self.op == "remove" and self.content is not None:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"remove of {self.surface_id!r} carries content; a remove deletes, it never writes"
|
|
83
|
+
)
|
|
84
|
+
if not self.rationale.strip():
|
|
85
|
+
raise ValueError(f"{self.op} of {self.surface_id!r} has no rationale")
|
|
86
|
+
return self
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class GateRecord(BaseModel):
|
|
90
|
+
"""The acceptance verdict, filled at evaluation time and persisted on the delta."""
|
|
91
|
+
|
|
92
|
+
suite_delta: float = 0.0 # regression suite: child - champion (tier 1; >= 0 to pass)
|
|
93
|
+
suite_fraction_delta: float = 0.0 # assertion credit; vetoes only when suite success ties
|
|
94
|
+
full_delta: float = 0.0 # full split: child - best seen (tier 2; >= 0 to pass)
|
|
95
|
+
full_fraction_delta: float = 0.0 # assertion credit; vetoes only when full success ties
|
|
96
|
+
holdout_delta: float | None = None # held-out split (tier 3; None when no holdout given)
|
|
97
|
+
holdout_fraction_delta: float | None = None # dense held-out signal when success ties
|
|
98
|
+
accepted: bool
|
|
99
|
+
reason: str # accept/reject reasoning, incl. whether `expected_effect` came true
|
|
100
|
+
|
|
101
|
+
@model_validator(mode="after")
|
|
102
|
+
def _validate_text(self) -> GateRecord:
|
|
103
|
+
validate_durable_text(self.reason, field="gate reason")
|
|
104
|
+
return self
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class HarnessDelta(BaseModel):
|
|
108
|
+
"""One proposed update to a `HarnessDoc`, with its audit trail.
|
|
109
|
+
|
|
110
|
+
Lineage is by content, not name: `parent_doc_hash` names exactly the document the delta was
|
|
111
|
+
proposed against, and `child_doc_hash` (recorded on successful application) names exactly what
|
|
112
|
+
it produced. A doc is reconstructable by folding accepted deltas from any ancestor snapshot.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
delta_id: str
|
|
116
|
+
parent_doc_hash: str
|
|
117
|
+
trigger: FailureSignature
|
|
118
|
+
# surface_id -> expected content hash of the parent surface the proposer read. Every
|
|
119
|
+
# replace/remove target MUST appear here; application atomically rejects on any mismatch.
|
|
120
|
+
preconditions: dict[str, str] = Field(default_factory=dict)
|
|
121
|
+
ops: list[SurfaceOp] = Field(min_length=1)
|
|
122
|
+
expected_effect: str # falsifiable: e.g. "the trigger cluster's tasks flip to pass"
|
|
123
|
+
child_doc_hash: str | None = None # set by apply_delta
|
|
124
|
+
verdict: GateRecord | None = None # set by the gate; None until evaluated
|
|
125
|
+
|
|
126
|
+
@model_validator(mode="after")
|
|
127
|
+
def _validate_text(self) -> HarnessDelta:
|
|
128
|
+
validate_durable_text(self.expected_effect, field="delta expected effect")
|
|
129
|
+
return self
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def compute_delta_id(parent_doc_hash: str, ops: list[SurfaceOp]) -> str:
|
|
133
|
+
"""Deterministic delta identity: what it does to what, independent of when it was proposed."""
|
|
134
|
+
joined = parent_doc_hash + "".join(
|
|
135
|
+
f"\x00{op.op}\x00{op.surface_id}\x00{op.content or ''}"
|
|
136
|
+
f"\x00{op.budget or ''}\x00{op.path or ''}"
|
|
137
|
+
for op in ops
|
|
138
|
+
)
|
|
139
|
+
return hashlib.blake2b(joined.encode("utf-8"), digest_size=16).hexdigest()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def apply_delta(parent: HarnessDoc, delta: HarnessDelta, child_name: str) -> HarnessDoc:
|
|
143
|
+
"""Apply `delta` to `parent` atomically; returns the child with `delta.child_doc_hash` set.
|
|
144
|
+
|
|
145
|
+
Every check runs before anything is built, and the child itself re-validates as a whole
|
|
146
|
+
`HarnessDoc` at construction — an invalid delta must fail here, before any eval budget is
|
|
147
|
+
spent on the variant it would have produced. Raises `ValueError` on any violation.
|
|
148
|
+
"""
|
|
149
|
+
_check_lineage(parent, delta)
|
|
150
|
+
_check_preconditions(parent, delta)
|
|
151
|
+
_check_ops(parent, delta)
|
|
152
|
+
|
|
153
|
+
surfaces = {s.id: s for s in parent.surfaces}
|
|
154
|
+
for op in delta.ops:
|
|
155
|
+
if op.op == "remove":
|
|
156
|
+
del surfaces[op.surface_id]
|
|
157
|
+
continue
|
|
158
|
+
existing = surfaces.get(op.surface_id)
|
|
159
|
+
# Narrowing only: SurfaceOp's model validator guarantees add carries a kind and
|
|
160
|
+
# add/replace carry content, and _check_ops guarantees replace targets exist.
|
|
161
|
+
kind = op.kind if op.kind is not None else existing.kind if existing else None
|
|
162
|
+
budget = op.budget if op.budget is not None else existing.budget if existing else None
|
|
163
|
+
# Carry the file path: a replace inherits the existing surface's path unless overridden,
|
|
164
|
+
# so mutating a vendored pi `code:src-...` surface stays a valid pathful CODE surface.
|
|
165
|
+
path = op.path if op.path is not None else existing.path if existing else None
|
|
166
|
+
if kind is None or op.content is None:
|
|
167
|
+
raise ValueError(f"malformed {op.op} of {op.surface_id!r}")
|
|
168
|
+
surfaces[op.surface_id] = Surface(
|
|
169
|
+
id=op.surface_id, kind=kind, content=op.content, budget=budget, path=path
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
child = HarnessDoc(name=child_name, surfaces=list(surfaces.values()))
|
|
173
|
+
delta.child_doc_hash = child.doc_hash
|
|
174
|
+
return child
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _check_lineage(parent: HarnessDoc, delta: HarnessDelta) -> None:
|
|
178
|
+
if delta.parent_doc_hash != parent.doc_hash:
|
|
179
|
+
raise ValueError(
|
|
180
|
+
f"delta {delta.delta_id} was proposed against doc {delta.parent_doc_hash[:12]}, "
|
|
181
|
+
f"not {parent.doc_hash[:12]} ({parent.name} v{parent.version})"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _check_preconditions(parent: HarnessDoc, delta: HarnessDelta) -> None:
|
|
186
|
+
for surface_id, expected in delta.preconditions.items():
|
|
187
|
+
surface = parent.surface(surface_id)
|
|
188
|
+
if surface is None:
|
|
189
|
+
raise ValueError(f"precondition on unknown surface {surface_id!r}")
|
|
190
|
+
if surface.content_hash != expected:
|
|
191
|
+
raise ValueError(
|
|
192
|
+
f"precondition mismatch on {surface_id!r}: expected {expected[:12]}, "
|
|
193
|
+
f"parent has {surface.content_hash[:12]} — the delta was drafted against "
|
|
194
|
+
"different content"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _check_ops(parent: HarnessDoc, delta: HarnessDelta) -> None:
|
|
199
|
+
targets = [op.surface_id for op in delta.ops]
|
|
200
|
+
duplicates = sorted({t for t in targets if targets.count(t) > 1})
|
|
201
|
+
if duplicates:
|
|
202
|
+
raise ValueError(f"multiple ops target the same surface(s): {duplicates}")
|
|
203
|
+
for op in delta.ops:
|
|
204
|
+
existing = parent.surface(op.surface_id)
|
|
205
|
+
if op.op == "add":
|
|
206
|
+
if existing is not None:
|
|
207
|
+
raise ValueError(f"add of {op.surface_id!r}: the surface already exists")
|
|
208
|
+
else:
|
|
209
|
+
if existing is None:
|
|
210
|
+
raise ValueError(f"{op.op} of unknown surface {op.surface_id!r}")
|
|
211
|
+
if op.kind is not None and op.kind is not existing.kind:
|
|
212
|
+
raise ValueError(
|
|
213
|
+
f"{op.op} of {op.surface_id!r} declares kind {op.kind.value!r}; the surface "
|
|
214
|
+
f"is {existing.kind.value!r}"
|
|
215
|
+
)
|
|
216
|
+
if op.surface_id not in delta.preconditions:
|
|
217
|
+
raise ValueError(
|
|
218
|
+
f"{op.op} of {op.surface_id!r} has no precondition: an update must assert "
|
|
219
|
+
"the content hash of every surface it replaces or removes"
|
|
220
|
+
)
|