loopmath 0.3.0__py3-none-any.whl → 0.3.2__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.
- loopmath/__init__.py +1 -1
- loopmath/adapters/evolve.py +137 -0
- loopmath/adapters/harness2.py +233 -17
- loopmath/adapters/openevolve.py +305 -0
- loopmath/adapters/plugin_eval.py +585 -0
- loopmath/adapters/shinkaevolve.py +358 -0
- loopmath/belief/commands.py +17 -1
- loopmath/belief/design.py +31 -1
- loopmath/belief/fit.py +11 -0
- loopmath/belief/integrated.py +139 -0
- loopmath/belief/lookahead.py +23 -2
- loopmath/belief/sampled.py +416 -0
- loopmath/belief/state.py +79 -16
- loopmath/cli.py +75 -18
- loopmath/cli_errors.py +121 -0
- loopmath/cli_registry.py +272 -19
- loopmath/compare/__init__.py +188 -50
- loopmath/compare/_thresholds_sw.py +439 -0
- loopmath/compare/calibrate.py +16 -2
- loopmath/compare/calibration.py +16 -3
- loopmath/compare/checks.py +144 -16
- loopmath/compare/commands.py +97 -27
- loopmath/compare/model.py +283 -69
- loopmath/compare/units.py +119 -0
- loopmath/compare/value.py +564 -0
- loopmath/gate/__init__.py +3 -0
- loopmath/gate/commands.py +15 -1
- loopmath/gate/core.py +58 -15
- loopmath/gate/inputs.py +10 -6
- loopmath/gate/winner.py +762 -0
- loopmath/gate/winner_cli.py +141 -0
- loopmath/gate/winner_model.py +305 -0
- loopmath/harness_runs/commands.py +180 -10
- loopmath/harness_runs/ocp.py +177 -37
- loopmath/harness_runs/rows.py +82 -14
- loopmath/integrations/__init__.py +5 -0
- loopmath/integrations/dspy.py +634 -0
- loopmath/output.py +5 -0
- loopmath/recommend/commands.py +13 -0
- loopmath/recommend_role.py +356 -0
- loopmath/screen/__init__.py +13 -0
- loopmath/screen/commands.py +416 -0
- loopmath/screen/design.py +97 -0
- loopmath/screen/edits.py +169 -0
- loopmath/screen/model.py +356 -0
- loopmath/screen/record.py +250 -0
- loopmath/store/budget.py +40 -6
- loopmath/store/commands.py +42 -17
- loopmath/store/config.py +8 -3
- loopmath/store/record.py +6 -2
- loopmath/store/status.py +5 -4
- loopmath/tasks/__init__.py +13 -0
- loopmath/tasks/commands.py +231 -0
- loopmath/tasks/data.py +176 -0
- loopmath/tasks/fit.py +225 -0
- loopmath/tasks/posterior.py +196 -0
- loopmath/tasks/select.py +211 -0
- loopmath/tasks/sim.py +34 -0
- loopmath/types.py +7 -0
- loopmath/views/common.py +14 -0
- loopmath/views/posterior.py +5 -1
- loopmath/views/runs.py +7 -4
- {loopmath-0.3.0.dist-info → loopmath-0.3.2.dist-info}/METADATA +43 -15
- {loopmath-0.3.0.dist-info → loopmath-0.3.2.dist-info}/RECORD +71 -42
- {loopmath-0.3.0.data → loopmath-0.3.2.data}/data/spec/ocp-v0.2.schema.json +0 -0
- {loopmath-0.3.0.data → loopmath-0.3.2.data}/data/spec/ocp-v0.schema.json +0 -0
- {loopmath-0.3.0.data → loopmath-0.3.2.data}/data/spec/ocp_conformance.py +0 -0
- {loopmath-0.3.0.dist-info → loopmath-0.3.2.dist-info}/WHEEL +0 -0
- {loopmath-0.3.0.dist-info → loopmath-0.3.2.dist-info}/entry_points.txt +0 -0
- {loopmath-0.3.0.dist-info → loopmath-0.3.2.dist-info}/licenses/LICENSE +0 -0
- {loopmath-0.3.0.dist-info → loopmath-0.3.2.dist-info}/top_level.txt +0 -0
loopmath/__init__.py
CHANGED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""What the OpenEvolve and ShinkaEvolve readers share: one harness-runs row per LLM call whose model was recorded
|
|
2
|
+
(an OpenEvolve child, a ShinkaEvolve patch query), with one `mutator` stage, and the outcome "this call wrote a child
|
|
3
|
+
that beat its parent".
|
|
4
|
+
|
|
5
|
+
The rows use the run log's `stages` (spec/harness-runs.md): the role is `mutator`, the piece `mutate`, the agent
|
|
6
|
+
the tool's name and the model the one that wrote the child. A verdict outcome says whether the child's score beat
|
|
7
|
+
its parent's by more than `min_delta`; `delta`, `parent_score` and `child_score` ride along as numbers. Nothing
|
|
8
|
+
the tool wrote as text (code, diffs, prompts, replies, feedback, logs) is copied: ext holds ids, numbers and
|
|
9
|
+
loopmath's own labels.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import hashlib
|
|
15
|
+
import math
|
|
16
|
+
import re
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
from typing import Any, Callable
|
|
20
|
+
|
|
21
|
+
ROLE = "mutator"
|
|
22
|
+
PIECE = "mutate"
|
|
23
|
+
EXPERIMENT_LABEL = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FormatError(ValueError):
|
|
27
|
+
"""An input the reader does not recognise, named with what it found."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NeedsFlag(ValueError):
|
|
31
|
+
"""The input is readable, but a flag must say what it does not: the message names the flag."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class TablePrice:
|
|
36
|
+
"""A price table the caller loaded, as a reader uses it: `usd(model, tokens)` gives the dollars for one
|
|
37
|
+
model's tokens (keys `in`, `cache_read`, `cache_write` and `out`), or None when the table has no row for the
|
|
38
|
+
model; `path` and `as_of` name the table in the notes. The readers never load or price a table themselves."""
|
|
39
|
+
|
|
40
|
+
usd: Callable[[str, dict[str, int]], float | None]
|
|
41
|
+
path: str
|
|
42
|
+
as_of: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class Result:
|
|
47
|
+
rows: list[dict[str, Any]]
|
|
48
|
+
format: str
|
|
49
|
+
skipped: list[dict[str, str]] = field(default_factory=list) # [{run_id, reason}]
|
|
50
|
+
counts: dict[str, int] = field(default_factory=dict)
|
|
51
|
+
experiments: list[dict[str, Any]] = field(default_factory=list)
|
|
52
|
+
notes: list[str] = field(default_factory=list)
|
|
53
|
+
accounting: dict[str, float] = field(default_factory=dict) # dollars: total, attributed, unattributed
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def number(value: Any) -> float | None:
|
|
57
|
+
"""A finite float, or None for anything else (bools are flags, not scores)."""
|
|
58
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
59
|
+
return None
|
|
60
|
+
value = float(value)
|
|
61
|
+
return value if math.isfinite(value) else None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def iso(epoch: Any) -> str | None:
|
|
65
|
+
"""Epoch seconds as RFC 3339 UTC, or None."""
|
|
66
|
+
value = number(epoch)
|
|
67
|
+
if value is None or value <= 0:
|
|
68
|
+
return None
|
|
69
|
+
return datetime.fromtimestamp(value, tz=timezone.utc).isoformat().replace("+00:00", "Z")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def experiment_id(prefix: str, root_id: str) -> str:
|
|
73
|
+
"""A stable experiment id from the run's first program: `<prefix>-` and 16 hex."""
|
|
74
|
+
return f"{prefix}-" + hashlib.sha256(root_id.encode("utf-8")).hexdigest()[:16]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def check_label(label: str | None) -> None:
|
|
78
|
+
if label is not None and not EXPERIMENT_LABEL.match(label):
|
|
79
|
+
raise NeedsFlag(f"--experiment {label!r}: letters, digits, '.', '_' or '-', at most 64")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def improved(child: float | None, parent: float | None, min_delta: float) -> tuple[bool, float | None]:
|
|
83
|
+
"""Whether the child beat its parent by more than `min_delta`, and the delta (None when either is unknown)."""
|
|
84
|
+
if child is None or parent is None:
|
|
85
|
+
return False, None
|
|
86
|
+
delta = child - parent
|
|
87
|
+
return delta > min_delta, delta
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def mutator_row(*, tool: str, ns: str, experiment: str, child_id: str, task_id: str, task: dict[str, Any],
|
|
91
|
+
model: str, effort: str | None, accepted: bool, delta: float | None, parent_score: float | None,
|
|
92
|
+
child_score: float | None, metric: str | None, cost: dict[str, Any] | None, status: str,
|
|
93
|
+
started: str | None, ended: str | None, stage_started: str | None, stage_ended: str | None,
|
|
94
|
+
stage: str, note: dict[str, Any]) -> dict[str, Any]:
|
|
95
|
+
"""One harness-runs row (spec/harness-runs.md) for one child: a single mutator stage."""
|
|
96
|
+
from ..harness_runs.rows import SCHEMA
|
|
97
|
+
|
|
98
|
+
outcome: dict[str, Any] = {"accepted": accepted, "by": tool}
|
|
99
|
+
if metric:
|
|
100
|
+
outcome["metric"] = metric
|
|
101
|
+
for key, value in (("delta", delta), ("parent_score", parent_score), ("child_score", child_score)):
|
|
102
|
+
if value is not None:
|
|
103
|
+
outcome[key] = value
|
|
104
|
+
st: dict[str, Any] = {"role": ROLE, "piece": PIECE, "status": status}
|
|
105
|
+
if cost:
|
|
106
|
+
st["cost"] = cost
|
|
107
|
+
if stage_started:
|
|
108
|
+
st["started_at"] = stage_started
|
|
109
|
+
if stage_ended:
|
|
110
|
+
st["ended_at"] = stage_ended
|
|
111
|
+
row: dict[str, Any] = {"schema": SCHEMA, "id": f"{tool}:{experiment}:{child_id}", "task_id": task_id,
|
|
112
|
+
"task": task, "experiment": experiment, "status": status, "agent": tool, "model": model,
|
|
113
|
+
"outcome": outcome, "stages": [st],
|
|
114
|
+
"tool": {"name": tool, "run_id": child_id, "task_id": task_id, "stage": stage},
|
|
115
|
+
"ext": {ns: {k: v for k, v in note.items() if v is not None}}}
|
|
116
|
+
if effort:
|
|
117
|
+
row["effort"] = effort
|
|
118
|
+
if started:
|
|
119
|
+
row["started_at"] = started
|
|
120
|
+
if ended:
|
|
121
|
+
row["ended_at"] = ended
|
|
122
|
+
return row
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def task_of(task_id: str, task_type: str | None, repo: str | None) -> dict[str, Any]:
|
|
126
|
+
return {"type": task_type or "research", "repo": repo or task_id}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def experiments(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
130
|
+
counts: dict[str, int] = {}
|
|
131
|
+
for r in rows:
|
|
132
|
+
counts[r["experiment"]] = counts.get(r["experiment"], 0) + 1
|
|
133
|
+
return [{"experiment": e, "runs": n} for e, n in sorted(counts.items())]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def plural(n: int, word: str, many: str | None = None) -> str:
|
|
137
|
+
return f"{n} {word if n == 1 else (many or word + 's')}"
|
loopmath/adapters/harness2.py
CHANGED
|
@@ -16,6 +16,7 @@ from __future__ import annotations
|
|
|
16
16
|
import csv
|
|
17
17
|
import hashlib
|
|
18
18
|
import json
|
|
19
|
+
import math
|
|
19
20
|
import re
|
|
20
21
|
from dataclasses import dataclass, field
|
|
21
22
|
from datetime import datetime, timezone
|
|
@@ -31,6 +32,15 @@ MARK = ".adapt_complete" # core/runmark.py:30
|
|
|
31
32
|
TOOL = "harness2"
|
|
32
33
|
TOOL_VERSION = "0.1.0" # pyproject.toml:7
|
|
33
34
|
NS = "dev.loopmath.adapter.harness2"
|
|
35
|
+
# harness2's own records, copied as it wrote them (the stage, the selection, a crashed mode's record). They hold
|
|
36
|
+
# text a model or harness2 wrote (editor notes, the selector's reason, the lesson, error text), so they sit
|
|
37
|
+
# outside loopmath's namespaces and a run that keeps them is `full`, never `metadata_only` (spec/harness-runs.md
|
|
38
|
+
# section 6, the privacy profile). They are kept only under privacy "full"; by default a row keeps their
|
|
39
|
+
# text-free part under NS (`stage`, `selection`, `crash_kind`) and the summary counts what was withheld.
|
|
40
|
+
RECORDS = "com.github.google-research.harness2"
|
|
41
|
+
PRIVACY = ("metadata_only", "full")
|
|
42
|
+
ERROR_KIND = re.compile(r"^([A-Z][A-Za-z0-9_]*): ")
|
|
43
|
+
TEXT_KEYS = ("error", "errors", "traceback") # summary members holding error text (core/schedule.py:654-659)
|
|
34
44
|
BENCHES = ("jb", "lab") # <runs>/<bench>/adapt (benchmarks/jobbench.py:512-513, lab.py:484-485)
|
|
35
45
|
WB_SUBSETS = ("code", "web", "office") # benchmarks/workbuddy.py:84-110; bench wb-<subset> at <runs>/wb/<subset> (653-658)
|
|
36
46
|
WB_STREAMS = {"oc": "agent/oc-output.txt", "cx": "agent/codex-output.jsonl"} # benchmarks/workbuddy.py:863-864
|
|
@@ -42,6 +52,10 @@ LABEL = re.compile(r"^(?P<kind>base|inv|dom|agg|c)(?P<n>\d+)$")
|
|
|
42
52
|
EXPERIMENT_LABEL = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$")
|
|
43
53
|
TRIAL = re.compile(r"^(?P<task>.+)__[A-Za-z0-9]+$") # a Harbor trial folder (benchmarks/workbuddy.py:451)
|
|
44
54
|
TIMESTAMP = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$")
|
|
55
|
+
# LAB metrics.json numbers and flags kept on the row (run_codex_lab.py:339-353; run_opencode.py:320-331); the
|
|
56
|
+
# token totals are not, since the row's cost is read from the session stream they sum
|
|
57
|
+
METRICS = ("turn_count", "tool_calls", "stream_errors", "wall_clock_seconds", "finished_cleanly", "exit_code",
|
|
58
|
+
"documents_read", "total_documents")
|
|
45
59
|
|
|
46
60
|
|
|
47
61
|
class FormatError(ValueError):
|
|
@@ -344,6 +358,22 @@ def _runner_record(lay: _Layout, sub: str, out: Path, traj: Path) -> dict[str, A
|
|
|
344
358
|
got["effort"] = cfg["reasoning_effort"]
|
|
345
359
|
if isinstance(cfg.get("started_at"), str) and TIMESTAMP.match(cfg["started_at"]):
|
|
346
360
|
got["started_at"] = cfg["started_at"]
|
|
361
|
+
note = {key: cfg[key] for key in ("codex_version", "doc_parser") if isinstance(cfg.get(key), str) and cfg[key]}
|
|
362
|
+
if _int(cfg.get("timeout")):
|
|
363
|
+
note["timeout_s"] = cfg["timeout"]
|
|
364
|
+
got["note"] = note
|
|
365
|
+
if lay.bench == "lab" and (out / "metrics.json").is_file(): # run_codex_lab.py:335-359, run_opencode.py:316-336
|
|
366
|
+
_confine(out / "metrics.json", lay.root, "file")
|
|
367
|
+
try:
|
|
368
|
+
met = json.loads((out / "metrics.json").read_text(encoding="utf-8"))
|
|
369
|
+
except (OSError, json.JSONDecodeError):
|
|
370
|
+
met = {}
|
|
371
|
+
if isinstance(met, dict):
|
|
372
|
+
kept = {key: met[key] for key in METRICS if isinstance(met.get(key), (int, float))}
|
|
373
|
+
if kept:
|
|
374
|
+
got.setdefault("note", {})["runner_metrics"] = kept
|
|
375
|
+
if isinstance(met.get("completed_at"), str) and TIMESTAMP.match(met["completed_at"]):
|
|
376
|
+
got["ended_at"] = met["completed_at"]
|
|
347
377
|
if lay.bench == "jb" and sub == "cx":
|
|
348
378
|
got["effort"] = "high"
|
|
349
379
|
index = traj / "attempt_index.tsv"
|
|
@@ -364,6 +394,10 @@ def _runner_record(lay: _Layout, sub: str, out: Path, traj: Path) -> dict[str, A
|
|
|
364
394
|
return got
|
|
365
395
|
|
|
366
396
|
|
|
397
|
+
def _int(value: Any) -> bool:
|
|
398
|
+
return isinstance(value, int) and not isinstance(value, bool)
|
|
399
|
+
|
|
400
|
+
|
|
367
401
|
def _experiment(lay: _Layout, agg_ns: str, config: Any, label: str | None) -> str:
|
|
368
402
|
"""A stable id for one experiment: SHA-256 over the bench, the agg namespace and the canonical
|
|
369
403
|
`config.json` (executor, improver, judge, tasks, method version, seed; cli/run_sweep.py:209-235), never
|
|
@@ -397,9 +431,11 @@ def experiment_id(runs_root: str | Path, agg_namespace: str, label: str | None =
|
|
|
397
431
|
def _score(n_passed: Any, n_criteria: Any, raw_score: Any = None) -> float | None:
|
|
398
432
|
"""core/judge.py:32-53: the fraction of criteria passed, when there are criteria. WorkBuddy's judge also
|
|
399
433
|
gives the verifier's own score, which n_passed rounds to hundredths (benchmarks/workbuddy.py:986-1004);
|
|
400
|
-
that score is used when it is a number in [0, 1].
|
|
401
|
-
|
|
402
|
-
|
|
434
|
+
that score is used when it is a number in [0, 1]. JobBench's weighted score gives both as floats (total and
|
|
435
|
+
max score, benchmarks/jobbench.py:598-599, 611-613), so a count need not be an int."""
|
|
436
|
+
if isinstance(n_criteria, (int, float)) and not isinstance(n_criteria, bool) and math.isfinite(n_criteria) \
|
|
437
|
+
and n_criteria > 0 and isinstance(n_passed, (int, float)) and not isinstance(n_passed, bool) \
|
|
438
|
+
and math.isfinite(n_passed):
|
|
403
439
|
if isinstance(raw_score, (int, float)) and not isinstance(raw_score, bool) and 0 <= raw_score <= 1:
|
|
404
440
|
return float(raw_score)
|
|
405
441
|
return float(n_passed) / n_criteria
|
|
@@ -407,10 +443,18 @@ def _score(n_passed: Any, n_criteria: Any, raw_score: Any = None) -> float | Non
|
|
|
407
443
|
|
|
408
444
|
|
|
409
445
|
def _outcome(score: float | None, judge: str, raw: dict[str, Any]) -> dict[str, Any]:
|
|
446
|
+
"""The judge's score dict as the row's outcome: the members the spec names, and the rest of what harness2's
|
|
447
|
+
score() returned (JobBench's criteria_passed, criteria_count and no_output, benchmarks/jobbench.py:611-619,
|
|
448
|
+
1060-1068) in `detail`, so nothing it recorded is dropped. `judge_model` is the outcome's judge and WorkBuddy's
|
|
449
|
+
`raw_score` its score (benchmarks/workbuddy.py:999-1004)."""
|
|
410
450
|
out: dict[str, Any] = {"name": "judge", "score": score, "judge": judge, "scale": "fraction", "better": "higher"}
|
|
411
451
|
for key in ("n_passed", "n_criteria", "all_pass", "metric"):
|
|
412
452
|
if raw.get(key) is not None:
|
|
413
453
|
out[key] = raw[key]
|
|
454
|
+
detail = {key: value for key, value in raw.items()
|
|
455
|
+
if key not in ("n_passed", "n_criteria", "all_pass", "metric", "judge_model", "raw_score")}
|
|
456
|
+
if detail:
|
|
457
|
+
out["detail"] = detail
|
|
414
458
|
return out
|
|
415
459
|
|
|
416
460
|
|
|
@@ -561,17 +605,132 @@ def _parent_label(mode: str, label: str) -> str | None:
|
|
|
561
605
|
return "base" if t == 1 else f"c{t - 1}"
|
|
562
606
|
|
|
563
607
|
|
|
564
|
-
def
|
|
608
|
+
def _no_path(sel: dict) -> dict:
|
|
609
|
+
"""A selection record as kept on the adopted row: without `chosen_run_dir`, an absolute path on the machine that
|
|
610
|
+
ran it (core/selection.py:48-65). The row's run id names the same folder."""
|
|
611
|
+
return {key: value for key, value in sel.items() if key != "chosen_run_dir"}
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _kind(error: Any) -> str | None:
|
|
615
|
+
"""An error's kind without its text: the exception class when it leads as `<Class>: ...` (core/schedule.py:654-657;
|
|
616
|
+
core/recursion.py:172, 296), else `other`; None for no error. The text names files and quotes model output."""
|
|
617
|
+
if not isinstance(error, str) or not error.strip():
|
|
618
|
+
return None
|
|
619
|
+
m = ERROR_KIND.match(error)
|
|
620
|
+
return m[1] if m else "other"
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def _meta_stage(rec: dict) -> dict:
|
|
624
|
+
"""A stage record's text-free part (core/recursion.py:79-104): its flags, attempts and the kind of its error.
|
|
625
|
+
`notes` and the error text are the editor's and stay out unless --privacy full."""
|
|
626
|
+
out = {key: rec[key] for key in ("dim", "attempts", "cached", "fell_back", "kept_base") if rec.get(key) is not None}
|
|
627
|
+
if _kind(rec.get("error")) is not None:
|
|
628
|
+
out["error_kind"] = _kind(rec.get("error"))
|
|
629
|
+
return out
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _meta_selection(sel: dict) -> dict:
|
|
633
|
+
"""A selection record's text-free part (core/selection.py:46-66; core/schedule.py:545-548): ids, flags and
|
|
634
|
+
counts. `why`, `experience`, the `dropped` causes and the error text stay out unless --privacy full."""
|
|
635
|
+
out = {key: sel[key] for key in ("chosen_id", "chosen_label", "chosen_is_base", "chosen_tree_is_base",
|
|
636
|
+
"n_candidates", "ok", "fallback", "trivial", "harness_fingerprint")
|
|
637
|
+
if sel.get(key) is not None}
|
|
638
|
+
if sel.get("evidence") in ("deliverable", "trajectory"): # core/selection.py:65, 151
|
|
639
|
+
out["evidence"] = sel["evidence"]
|
|
640
|
+
if isinstance(sel.get("dropped"), list):
|
|
641
|
+
out["n_dropped"] = len(sel["dropped"])
|
|
642
|
+
out["has_experience"] = bool(sel.get("experience"))
|
|
643
|
+
if _kind(sel.get("error")) is not None:
|
|
644
|
+
out["error_kind"] = _kind(sel.get("error"))
|
|
645
|
+
return out
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _texts(value: Any) -> bool:
|
|
649
|
+
"""Whether a value holds any string, the test the privacy profile applies (harness_runs/ocp.py retained_text)."""
|
|
650
|
+
if isinstance(value, str):
|
|
651
|
+
return True
|
|
652
|
+
if isinstance(value, dict):
|
|
653
|
+
return any(_texts(v) for v in value.values())
|
|
654
|
+
if isinstance(value, list):
|
|
655
|
+
return any(_texts(v) for v in value)
|
|
656
|
+
return False
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _recorded_dead(run_dir: Any, task_dir: str) -> bool:
|
|
660
|
+
"""A rollout harness2 recorded dead: its run_dir is `<namespace>/<task dir>/missing` (core/schedule.py:424-427,
|
|
661
|
+
622; core/recursion.py:293). Its error is not written anywhere (no RunView is serialized)."""
|
|
662
|
+
parts = PurePosixPath(str(run_dir or "")).parts
|
|
663
|
+
return len(parts) >= 2 and parts[-2:] == (task_dir, "missing")
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _stage_order(mode: str, label: str) -> tuple:
|
|
667
|
+
"""Ledger order (core/recursion.py:366-425, 476-562): parallel inv<i>, dom<i> by i, then agg<i>;
|
|
668
|
+
sequential inv<t>, dom<t>, c<t> by round."""
|
|
669
|
+
m = LABEL.match(label)
|
|
670
|
+
t, kind = int(m["n"]), m["kind"]
|
|
671
|
+
if mode == "agg":
|
|
672
|
+
return (1, t, 0) if kind == "agg" else (0, t, 0 if kind == "inv" else 1)
|
|
673
|
+
return (0, t, ("inv", "dom", "c").index(kind))
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def _rolled(harnesses: Path, mode: str) -> list[str]:
|
|
677
|
+
"""The stages a mode rolled, from its `harnesses/<label>` folders (each written right before its rollout,
|
|
678
|
+
core/recursion.py:274-276), for a mode whose crash record replaced its ledger (core/schedule.py:655-659)."""
|
|
679
|
+
if not harnesses.is_dir():
|
|
680
|
+
return []
|
|
681
|
+
kinds = ("inv", "dom", "agg") if mode == "agg" else ("inv", "dom", "c")
|
|
682
|
+
labels = [p.name for p in harnesses.iterdir()
|
|
683
|
+
if p.is_dir() and (m := LABEL.match(p.name)) is not None and m["kind"] in kinds]
|
|
684
|
+
return sorted(labels, key=lambda label: _stage_order(mode, label))
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def _sweep(lay: _Layout, agg_ns: str) -> dict[str, Any] | None:
|
|
688
|
+
"""The newest sweep record of a namespace, `adapt/_sweeps/<namespace>-<UTC stamp>.json`, written once per
|
|
689
|
+
`harness2-run-sweep` after the sweep returns (cli/run_sweep.py:175-178); None when there is none."""
|
|
690
|
+
folder = lay.adapt / "_sweeps"
|
|
691
|
+
stamp = re.compile(re.escape(agg_ns) + r"-\d{8}T\d{6}Z\.json$")
|
|
692
|
+
found = sorted(p for p in folder.glob(f"{agg_ns}-*.json") if stamp.match(p.name)) if folder.is_dir() else []
|
|
693
|
+
if not found:
|
|
694
|
+
return None
|
|
695
|
+
doc = _load(found[-1], lay.root)
|
|
696
|
+
return doc if isinstance(doc, dict) else None
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def _sweep_meta(doc: dict[str, Any] | None) -> tuple[dict[str, Any] | None, int]:
|
|
700
|
+
"""A sweep record without its error text (core/schedule.py:724-729, 835: a domain's or a task's `error`), and how
|
|
701
|
+
many errors it held."""
|
|
702
|
+
if not isinstance(doc, dict) or not isinstance(doc.get("domains"), list):
|
|
703
|
+
return doc, 0
|
|
704
|
+
held, domains = 0, []
|
|
705
|
+
for dom in doc["domains"]:
|
|
706
|
+
if isinstance(dom, dict):
|
|
707
|
+
held += bool(dom.get("error"))
|
|
708
|
+
dom = {key: value for key, value in dom.items() if key != "error"}
|
|
709
|
+
if isinstance(dom.get("tasks"), list):
|
|
710
|
+
held += sum(1 for t in dom["tasks"] if isinstance(t, dict) and t.get("error"))
|
|
711
|
+
dom["tasks"] = [{key: value for key, value in t.items() if key != "error"} if isinstance(t, dict)
|
|
712
|
+
else t for t in dom["tasks"]]
|
|
713
|
+
domains.append(dom)
|
|
714
|
+
return {**doc, "domains": domains}, held
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def read(path: str | Path, *, experiment: str | None = None, privacy: str = "metadata_only") -> Result:
|
|
565
718
|
"""Every rollout under a harness2 runs folder, as checked harness-runs rows. Reads only; writes nothing.
|
|
566
719
|
|
|
567
720
|
Raises FormatError for a folder with no harness2 runs, an unknown method or marker version, or a
|
|
568
721
|
summary missing its keys, naming what it found. `experiment` labels repeats of one configuration.
|
|
722
|
+
`privacy` "metadata_only" (the default) keeps no text a model or harness2 wrote: harness2's own records
|
|
723
|
+
and any text in a judge's extra members are withheld and counted (`records_withheld`, `text_withheld`).
|
|
724
|
+
"full" keeps them, and the rows that carry them become `full` documents.
|
|
569
725
|
"""
|
|
570
726
|
from ..harness_runs.rows import SCHEMA, check_row
|
|
571
727
|
from ..harness_runs.streams import rollout_usage
|
|
572
728
|
|
|
573
729
|
if experiment is not None and not EXPERIMENT_LABEL.match(experiment):
|
|
574
730
|
raise ValueError(f"--experiment {experiment!r}: letters, digits, '.', '_' or '-', at most 64")
|
|
731
|
+
if privacy not in PRIVACY:
|
|
732
|
+
raise ValueError(f"privacy {privacy!r}: one of {', '.join(PRIVACY)}")
|
|
733
|
+
full = privacy == "full"
|
|
575
734
|
path = Path(path)
|
|
576
735
|
layouts = _layouts(path)
|
|
577
736
|
unsupported = _unsupported(_runs_root(path)) if not layouts or layouts[0].kind == "bench" else []
|
|
@@ -586,7 +745,7 @@ def read(path: str | Path, *, experiment: str | None = None) -> Result:
|
|
|
586
745
|
counts = {k: 0 for k in ("rollouts", "bases", "stages", "adopted", "scored", "unscored", "failed",
|
|
587
746
|
"unavailable", "stale_judge", "bare_base", "mode_failed", "kept_base", "fallback",
|
|
588
747
|
"no_config", "fingerprint_mismatch", "parent_unverified", "unsupported",
|
|
589
|
-
"unread_namespace", "cost_unread")}
|
|
748
|
+
"unread_namespace", "cost_unread", "records_withheld", "text_withheld")}
|
|
590
749
|
for name in unsupported:
|
|
591
750
|
counts["unsupported"] += 1
|
|
592
751
|
result.skipped.append({"run_id": f"{name}/adapt", "reason": f"unsupported bench folder: this importer "
|
|
@@ -623,8 +782,11 @@ def read(path: str | Path, *, experiment: str | None = None) -> Result:
|
|
|
623
782
|
result.notes.append(f"{_rel(ns_dir, root)}: WorkBuddy rollouts carry no cost: the agent stream "
|
|
624
783
|
f"({WB_STREAMS.get(agg['sub'], 'agent/')}) is written by Harbor and this "
|
|
625
784
|
f"importer does not read its usage; each row names it as cost_unread")
|
|
785
|
+
tasks: dict[str, Any] = {}
|
|
786
|
+
sweep, held = (_sweep(lay, ns_dir.name), 0) if full else _sweep_meta(_sweep(lay, ns_dir.name))
|
|
787
|
+
counts["text_withheld"] += held
|
|
626
788
|
result.experiments.append({"experiment": exp, "bench": lay.bench, "namespace": ns_dir.name,
|
|
627
|
-
"config": config
|
|
789
|
+
"config": config, "sweep": sweep, "tasks": tasks})
|
|
628
790
|
judges = _Judges(ns_dir, root, legacy)
|
|
629
791
|
agent, kind = AGENTS.get(agg["sub"], (agg["sub"], None))
|
|
630
792
|
if lay.kind == "demo":
|
|
@@ -635,6 +797,15 @@ def read(path: str | Path, *, experiment: str | None = None) -> Result:
|
|
|
635
797
|
legacy.extend([True] if summary["_legacy"] else [])
|
|
636
798
|
task_id = _check_id(str(summary["task_id"]), "task_id", spath, root)
|
|
637
799
|
task_dir = spath.parent.name # core/schedule.py:101-102
|
|
800
|
+
drop = ("task_id", "base", "selections", "_legacy") + (() if full else TEXT_KEYS)
|
|
801
|
+
tasks[task_id] = {key: value for key, value in summary.items() if key not in drop}
|
|
802
|
+
tasks[task_id]["modes"] = {m: {key: value for key, value in rec.items()
|
|
803
|
+
if key != "stages" and (full or key not in TEXT_KEYS)}
|
|
804
|
+
if isinstance(rec, dict) else rec for m, rec in summary["modes"].items()}
|
|
805
|
+
if not full:
|
|
806
|
+
counts["text_withheld"] += sum(1 for key in TEXT_KEYS if summary.get(key)) + sum(
|
|
807
|
+
1 for rec in summary["modes"].values() if isinstance(rec, dict)
|
|
808
|
+
for key in TEXT_KEYS if rec.get(key))
|
|
638
809
|
k = summary["k"] if isinstance(summary["k"], int) else agg["k"]
|
|
639
810
|
plan: list[dict[str, Any]] = []
|
|
640
811
|
base_ids = {}
|
|
@@ -651,20 +822,33 @@ def read(path: str | Path, *, experiment: str | None = None) -> Result:
|
|
|
651
822
|
plan.append({"run_id": rid, "label": label, "mode": "agg", "ns": ns_dir.name, "arm": "base",
|
|
652
823
|
"tree": ns_dir / task_dir / "harnesses" / "base", "parent": None, "bare":
|
|
653
824
|
not isinstance(entry, dict),
|
|
654
|
-
"run_dir": str(entry.get("run_dir") or "") if isinstance(entry, dict) else ""
|
|
825
|
+
"run_dir": str(entry.get("run_dir") or "") if isinstance(entry, dict) else "",
|
|
826
|
+
"dead": isinstance(entry, dict) and _recorded_dead(entry.get("run_dir"), task_dir)})
|
|
655
827
|
for item in plan: # one batch's markers all name its first base (workbuddy.py:853-854)
|
|
656
828
|
item["mark_ids"] = set(base_ids.values())
|
|
657
829
|
for mode, rec in sorted(summary["modes"].items()):
|
|
658
830
|
_check_id(mode, "mode", spath, root, part=True)
|
|
659
831
|
mode_ns = _mode_ns(agg, mode)
|
|
660
832
|
mode_dir = lay.adapt / mode_ns / task_dir
|
|
833
|
+
crash = None
|
|
661
834
|
if not isinstance(rec, dict) or not isinstance(rec.get("ledger"), dict):
|
|
662
835
|
counts["mode_failed"] += 1 # core/schedule.py:655-659
|
|
663
836
|
why = (rec or {}).get("error") if isinstance(rec, dict) else None
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
837
|
+
why = why if full or not why else f"{_kind(why)} (its text is kept under --privacy full)"
|
|
838
|
+
rolled = _rolled(mode_dir / "harnesses", mode)
|
|
839
|
+
result.skipped.append({"run_id": f"{mode_ns}/{task_id}", "reason": (
|
|
840
|
+
f"the {mode} mode failed and left no ledger: {why or 'no record'}"
|
|
841
|
+
+ (f"; its {len(rolled)} rollouts are read from {_rel(mode_dir / 'harnesses', root)} "
|
|
842
|
+
f"(written before each rollout, core/recursion.py:274-276), each with the crash record"
|
|
843
|
+
if rolled else ""))})
|
|
844
|
+
if not rolled:
|
|
845
|
+
continue
|
|
846
|
+
# the traceback names files on the machine that ran it, so only the rest is kept
|
|
847
|
+
crash = {k: v for k, v in rec.items() if k != "traceback"} if isinstance(rec, dict) \
|
|
848
|
+
else {"error": None}
|
|
849
|
+
rec = {"ledger": {"rollouts": rolled}}
|
|
850
|
+
stage_rec = {s.get("label"): s for s in rec.get("stages") or [] if isinstance(s, dict)}
|
|
851
|
+
stage_usd = {label: s.get("usd") for label, s in stage_rec.items()}
|
|
668
852
|
sel = summary["selections"].get(mode) or {}
|
|
669
853
|
if isinstance(sel, dict) and "mode" not in sel and "arm" in sel:
|
|
670
854
|
legacy.append(True)
|
|
@@ -692,7 +876,8 @@ def read(path: str | Path, *, experiment: str | None = None) -> Result:
|
|
|
692
876
|
"parent": None if parent is None else mode_dir / "harnesses" / parent,
|
|
693
877
|
"parent_run_id": prid,
|
|
694
878
|
"parent_run_dir": recorded if prid is not None and prid == chosen else None,
|
|
695
|
-
"editor_usd": stage_usd.get(label),
|
|
879
|
+
"editor_usd": stage_usd.get(label), "stage_record": stage_rec.get(label),
|
|
880
|
+
"selection_record": _no_path(sel) if rid == chosen else None, "mode_crash": crash,
|
|
696
881
|
"selector_usd": sel.get("usd") if rid == chosen else None,
|
|
697
882
|
"selection_label": sel.get("chosen_label") if rid == chosen else None,
|
|
698
883
|
"run_dir": recorded if rid == chosen else None})
|
|
@@ -702,7 +887,7 @@ def read(path: str | Path, *, experiment: str | None = None) -> Result:
|
|
|
702
887
|
continue
|
|
703
888
|
seen.add(rid)
|
|
704
889
|
row = _row(lay, agg, item, task_id, k, exp, experiment, executor, judge_name, agent, kind,
|
|
705
|
-
judges, counts, result, rollout_usage)
|
|
890
|
+
judges, counts, result, rollout_usage, full=full)
|
|
706
891
|
if row is None:
|
|
707
892
|
continue
|
|
708
893
|
row["schema"] = SCHEMA
|
|
@@ -722,7 +907,7 @@ def read(path: str | Path, *, experiment: str | None = None) -> Result:
|
|
|
722
907
|
|
|
723
908
|
|
|
724
909
|
def _row(lay, agg, item, task_id, k, exp, label, executor, judge_name, agent, kind, judges, counts, result,
|
|
725
|
-
rollout_usage) -> dict[str, Any] | None:
|
|
910
|
+
rollout_usage, *, full: bool = False) -> dict[str, Any] | None:
|
|
726
911
|
rid, is_base = item["run_id"], item["arm"] == "base"
|
|
727
912
|
tree = item["tree"]
|
|
728
913
|
if tree.is_dir():
|
|
@@ -738,8 +923,14 @@ def _row(lay, agg, item, task_id, k, exp, label, executor, judge_name, agent, ki
|
|
|
738
923
|
out, traj = _rollout_dirs(lay, agg["sub"], task_id, rid)
|
|
739
924
|
if not out.is_dir() and not traj.is_dir():
|
|
740
925
|
counts["unavailable"] += 1
|
|
741
|
-
|
|
742
|
-
|
|
926
|
+
skip = {"run_id": rid, "reason": (
|
|
927
|
+
f"unavailable: no run folder at {_rel(out, lay.root)} (copied without it, or the rollout died "
|
|
928
|
+
f"before its runner made one); harness2 records no rollout error (it writes no RunView, "
|
|
929
|
+
f"core/recursion.py:79-105)" + ("; harness2 recorded it dead (run_dir <task dir>/missing, "
|
|
930
|
+
"core/schedule.py:424-427)" if item.get("dead") else ""))}
|
|
931
|
+
if isinstance(item.get("stage_record"), dict): # the stage's editor session was paid even so
|
|
932
|
+
skip["stage"] = item["stage_record"] if full else _meta_stage(item["stage_record"])
|
|
933
|
+
result.skipped.append(skip)
|
|
743
934
|
return None
|
|
744
935
|
mark = _mark(traj, lay.root) if lay.kind == "bench" else None
|
|
745
936
|
ok_ids = {rid, *item.get("mark_ids", ())} if _is_wb(lay) and is_base else {rid}
|
|
@@ -846,6 +1037,31 @@ def _row(lay, agg, item, task_id, k, exp, label, executor, judge_name, agent, ki
|
|
|
846
1037
|
note["parent_changed"] = True
|
|
847
1038
|
elif unverified and record is not None:
|
|
848
1039
|
note["parent_unverified"] = unverified
|
|
1040
|
+
if mark is not None: # core/runmark.py:69-75; benchmarks/jobbench.py:755
|
|
1041
|
+
if isinstance(mark.get("at"), (int, float)) and not isinstance(mark.get("at"), bool):
|
|
1042
|
+
note["marked_at"] = _iso(float(mark["at"]))
|
|
1043
|
+
if "output_fingerprint" in mark:
|
|
1044
|
+
note["output_fingerprint"] = mark["output_fingerprint"]
|
|
1045
|
+
note.update(runner.get("note") or {})
|
|
1046
|
+
if item.get("mode_crash") is not None:
|
|
1047
|
+
note["mode_crashed"] = True
|
|
1048
|
+
if _kind(item["mode_crash"].get("error")) is not None:
|
|
1049
|
+
note["crash_kind"] = _kind(item["mode_crash"].get("error"))
|
|
1050
|
+
for key, meta in (("stage", _meta_stage), ("selection", _meta_selection)):
|
|
1051
|
+
if isinstance(item.get(f"{key}_record"), dict):
|
|
1052
|
+
note[key] = meta(item[f"{key}_record"])
|
|
1053
|
+
records = {key: item[f"{key}_record"] for key in ("stage", "selection") if isinstance(item.get(f"{key}_record"), dict)}
|
|
1054
|
+
if item.get("mode_crash") is not None:
|
|
1055
|
+
records["mode_crash"] = item["mode_crash"]
|
|
1056
|
+
if records and not full:
|
|
1057
|
+
counts["records_withheld"] += len(records)
|
|
1058
|
+
records = {}
|
|
1059
|
+
if not full and isinstance(outcome, dict) and isinstance(outcome.get("detail"), dict):
|
|
1060
|
+
texts = [key for key, value in outcome["detail"].items() if _texts(value)]
|
|
1061
|
+
counts["text_withheld"] += len(texts)
|
|
1062
|
+
outcome["detail"] = {key: value for key, value in outcome["detail"].items() if key not in texts}
|
|
1063
|
+
if not outcome["detail"]:
|
|
1064
|
+
del outcome["detail"]
|
|
849
1065
|
if _is_wb(lay):
|
|
850
1066
|
counts["cost_unread"] += 1
|
|
851
1067
|
note["cost_unread"] = (f"{WB_STREAMS.get(agg['sub'], 'the agent stream')} is Harbor's and its usage is not "
|
|
@@ -857,7 +1073,7 @@ def _row(lay, agg, item, task_id, k, exp, label, executor, judge_name, agent, ki
|
|
|
857
1073
|
"outcome": outcome, "cost": cost,
|
|
858
1074
|
"tool": {"name": TOOL, "version": TOOL_VERSION, "run_id": rid, "task_id": task_id,
|
|
859
1075
|
"stage": item["label"]},
|
|
860
|
-
"ext": {NS: note}}
|
|
1076
|
+
"ext": {NS: note, **({RECORDS: records} if records else {})}}
|
|
861
1077
|
if runner.get("effort"):
|
|
862
1078
|
row["effort"] = runner["effort"]
|
|
863
1079
|
if started:
|