agent-harness / src /agent_harness /study5_experiment.py
cuber12's picture
Publish agent harness research code and paper artifacts
d61821a verified
Raw
History Blame Contribute Delete
9.83 kB
"""Manifest-driven prospective Study 5 harness-factor experiments."""
from __future__ import annotations
from hashlib import sha256
import json
from pathlib import Path
import time
from typing import Any
from .lm_studio_embeddings import LMStudioEmbeddingClient
from .lm_studio_management import LMStudioResidencyManager, LMStudioServer
from .pilot import research_code_revision
from .protocol_experiment import (
ProtocolExperimentError,
_build_task_retrieval,
_repository_for_task,
run_protocol_cell,
)
from .repository import GitSnapshot
from .retrieval import SQLiteEmbeddingCache
from .specs import (
load_edit_interfaces,
load_embeddings,
load_experiments,
load_harnesses,
load_models,
load_repositories,
load_tasks,
)
from .study2_experiment import _RuntimeLease
class Study5ExperimentError(RuntimeError):
"""Raised when a Study 5 manifest or runtime violates its frozen design."""
def _manifest_hash(value: dict[str, Any]) -> str:
payload = dict(value)
expected = payload.pop("design_sha256", None)
observed = sha256(
json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
).hexdigest()
if expected != observed:
raise Study5ExperimentError(
f"Study 5 manifest hash mismatch: expected {expected}, observed {observed}"
)
return observed
def _write_progress(
root: Path,
experiment_id: str,
revision: str,
manifest_hash: str,
planned: int,
rows: list[dict[str, Any]],
) -> Path:
path = root / "results" / "reports" / f"{experiment_id}_progress.json"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
json.dumps(
{
"schema_version": 1,
"experiment_id": experiment_id,
"code_revision": revision,
"manifest_sha256": manifest_hash,
"planned_cells": planned,
"completed_cells": len(rows),
"accepted_edit_cells": sum(bool(row["accepted_edit_cell"]) for row in rows),
"applicable_patch_cells": sum(bool(row["applicable_final_patch"]) for row in rows),
"resolved_cells": sum(bool(row["resolved_at_1"]) for row in rows),
"rows": rows,
},
indent=2,
sort_keys=True,
)
+ "\n",
encoding="utf-8",
)
return path
def run_study5_experiment(
root: Path,
experiment_id: str,
task_filter: set[str] | None = None,
harness_filter: set[str] | None = None,
interface_filter: set[str] | None = None,
model_filter: set[str] | None = None,
stop_server_when_complete: bool = True,
) -> dict[str, Any]:
if experiment_id not in {"E13", "E14", "E15", "E16"}:
raise Study5ExperimentError("Study 5 runner requires E13, E14, E15, or E16")
revision = research_code_revision(root)
experiment = load_experiments(root)[experiment_id]
manifest_path = root / "configs" / "study5" / f"{experiment_id}_cells.json"
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
manifest_hash = _manifest_hash(manifest)
if manifest.get("experiment_id") != experiment_id or not manifest.get("outcome_blind"):
raise Study5ExperimentError("Study 5 manifest identity/freeze flag mismatch")
all_cells = manifest.get("cells")
if not isinstance(all_cells, list) or len(all_cells) != int(manifest["planned_cells"]):
raise Study5ExperimentError("Study 5 manifest cell count mismatch")
tasks = load_tasks(root)
harnesses = load_harnesses(root)
interfaces = load_edit_interfaces(root)
models = load_models(root)
repositories = load_repositories(root)
embedding = load_embeddings(root)[experiment.embedding_id]
cells = [
item
for item in all_cells
if (task_filter is None or item["task_id"] in task_filter)
and (harness_filter is None or item["harness_id"] in harness_filter)
and (interface_filter is None or item["interface_id"] in interface_filter)
and (model_filter is None or item["model_id"] in model_filter)
]
if not cells:
raise Study5ExperimentError("Study 5 filters selected an empty execution block")
identities: set[tuple[str, str, str, str]] = set()
for item in cells:
task = tasks[item["task_id"]]
harness = harnesses[item["harness_id"]]
interface = interfaces[item["interface_id"]]
model = models[item["model_id"]]
identity = (task.task_id, harness.harness_id, interface.interface_id, model.model_id)
if identity in identities:
raise Study5ExperimentError(f"duplicate Study 5 cell: {identity}")
identities.add(identity)
expected = (
task.base_commit,
harness.config_hash,
interface.config_hash,
model.config_hash,
)
observed = (
item["repository_sha"],
item["harness_hash"],
item["interface_hash"],
item["model_hash"],
)
if observed != expected:
raise Study5ExperimentError(f"frozen configuration drift for {identity}")
if task.validation_status != "end_to_end_ready":
raise Study5ExperimentError(f"{task.task_id} is not end-to-end ready")
server = LMStudioServer(port=1234)
first_model = models[cells[0]["model_id"]]
residency = LMStudioResidencyManager(
first_model.base_url,
first_model.api_token_env,
timeout_seconds=experiment.timeout_seconds,
)
embedding_client = LMStudioEmbeddingClient(
embedding, timeout_seconds=experiment.timeout_seconds
)
cache_path = root / "indexes" / "embeddings" / f"{embedding.config_hash}.sqlite3"
rows: list[dict[str, Any]] = []
task_summaries: list[dict[str, Any]] = []
runtime = _RuntimeLease(server, residency, stop_server_when_complete)
grouped: dict[str, list[dict[str, Any]]] = {}
for cell in cells:
grouped.setdefault(str(cell["task_id"]), []).append(cell)
with runtime as server_state, SQLiteEmbeddingCache(cache_path, embedding) as cache:
for task_id, task_cells in grouped.items():
task = tasks[task_id]
repository_spec = _repository_for_task(repositories, task)
repository = (root / repository_spec.local_path).resolve()
snapshot = GitSnapshot(repository)
snapshot.verify_commit(task.base_commit)
index_transition = residency.ensure_exclusive(
embedding.model_key, embedding.loaded_context_length
)
embedding_client.resolve()
index_started = time.monotonic()
retrieval = _build_task_retrieval(snapshot, task, embedding, embedding_client, cache)
index_elapsed = time.monotonic() - index_started
task_rows: list[dict[str, Any]] = []
for cell in sorted(task_cells, key=lambda item: int(item["order"])):
row = run_protocol_cell(
root,
repository,
experiment,
task,
interfaces[cell["interface_id"]],
models[cell["model_id"]],
residency,
server,
revision,
retrieval_harness=harnesses[cell["harness_id"]],
retrieval=retrieval,
embedding=embedding,
seed=int(cell["seed"]),
context_budget=int(cell["context_budget"]),
)
rows.append(row)
task_rows.append(row)
_write_progress(
root, experiment_id, revision, manifest_hash, len(cells), rows
)
task_summaries.append(
{
"task_id": task_id,
"repository_id": repository_spec.repository_id,
"language": task.language,
"cells": len(task_rows),
"accepted_edits": sum(bool(row["accepted_edit_cell"]) for row in task_rows),
"applicable_patches": sum(bool(row["applicable_final_patch"]) for row in task_rows),
"resolved": sum(bool(row["resolved_at_1"]) for row in task_rows),
"embedding_index_transition": index_transition.to_dict(),
"index_elapsed_seconds": index_elapsed,
"dense_index_stats": retrieval.dense_index_stats,
}
)
if len(rows) != len(cells):
raise Study5ExperimentError(f"finalized {len(rows)}/{len(cells)} selected cells")
report = {
"schema_version": 1,
"experiment_id": experiment_id,
"code_revision": revision,
"manifest_sha256": manifest_hash,
"planned_cells": len(cells),
"run_count": len(rows),
"accepted_edit_count": sum(bool(row["accepted_edit_cell"]) for row in rows),
"applicable_patch_count": sum(bool(row["applicable_final_patch"]) for row in rows),
"resolved_count": sum(bool(row["resolved_at_1"]) for row in rows),
"server_lifecycle": server_state,
"server_stop": runtime.stop_state,
"final_residency_transition": runtime.final_transition,
"cleanup_errors": runtime.cleanup_errors,
"task_summaries": task_summaries,
"rows": rows,
}
report_path = (
root
/ "results"
/ "reports"
/ f"{experiment_id}_{revision[:12]}_{int(time.time())}.json"
)
report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
return {**report, "report_path": str(report_path)}