traceverdict 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.
- traceverdict/__init__.py +3 -0
- traceverdict/adapters/__init__.py +1 -0
- traceverdict/adapters/codex.py +235 -0
- traceverdict/adapters/mini_swe_agent.py +365 -0
- traceverdict/adapters/swe_agent.py +437 -0
- traceverdict/adapters/swe_agent_entrypoint.py +119 -0
- traceverdict/cli.py +309 -0
- traceverdict/compare/__init__.py +314 -0
- traceverdict/compare/constants.py +12 -0
- traceverdict/core/__init__.py +1 -0
- traceverdict/core/config_loader.py +72 -0
- traceverdict/core/runner.py +570 -0
- traceverdict/core/selftest.py +393 -0
- traceverdict/core/simple_yaml.py +252 -0
- traceverdict/core/suite.py +77 -0
- traceverdict/core/task_loader.py +69 -0
- traceverdict/cost_ledger.py +47 -0
- traceverdict/daily.py +397 -0
- traceverdict/ingest.py +272 -0
- traceverdict/injections/__init__.py +134 -0
- traceverdict/m3.py +484 -0
- traceverdict/m4c.py +134 -0
- traceverdict/m4d.py +381 -0
- traceverdict/m4s.py +154 -0
- traceverdict/report/__init__.py +128 -0
- traceverdict/report/taxonomy.py +76 -0
- traceverdict/resources/daily/configs/dev.yaml +15 -0
- traceverdict/resources/daily/configs/litellm_models.json +18 -0
- traceverdict/resources/daily/configs/litellm_models.meta.json +30 -0
- traceverdict/resources/daily/tasks/self/S1/BASE_COMMIT.txt +1 -0
- traceverdict/resources/daily/tasks/self/S1/repo.bundle +0 -0
- traceverdict/resources/daily/tasks/self/S1/task.yaml +26 -0
- traceverdict/resources/daily/tasks/self/S1/verify/README.md +11 -0
- traceverdict/resources/daily/tasks/self/S2/BASE_COMMIT.txt +1 -0
- traceverdict/resources/daily/tasks/self/S2/repo.bundle +0 -0
- traceverdict/resources/daily/tasks/self/S2/task.yaml +24 -0
- traceverdict/resources/daily/tasks/self/S2/verify/README.md +1 -0
- traceverdict/resources/daily/tasks/self/S3/BASE_COMMIT.txt +1 -0
- traceverdict/resources/daily/tasks/self/S3/repo.bundle +0 -0
- traceverdict/resources/daily/tasks/self/S3/task.yaml +24 -0
- traceverdict/resources/daily/tasks/self/S3/verify/README.md +1 -0
- traceverdict/resources/daily/tasks/self/S4/BASE_COMMIT.txt +1 -0
- traceverdict/resources/daily/tasks/self/S4/repo.bundle +0 -0
- traceverdict/resources/daily/tasks/self/S4/task.yaml +24 -0
- traceverdict/resources/daily/tasks/self/S4/verify/README.md +1 -0
- traceverdict/resources/daily/tasks/self/S5/BASE_COMMIT.txt +1 -0
- traceverdict/resources/daily/tasks/self/S5/repo.bundle +0 -0
- traceverdict/resources/daily/tasks/self/S5/task.yaml +26 -0
- traceverdict/resources/daily/tasks/self/S5/verify/README.md +1 -0
- traceverdict/resources/daily/tasks/self/S6/BASE_COMMIT.txt +1 -0
- traceverdict/resources/daily/tasks/self/S6/repo.bundle +0 -0
- traceverdict/resources/daily/tasks/self/S6/task.yaml +29 -0
- traceverdict/resources/daily/tasks/self/S6/verify/README.md +3 -0
- traceverdict/resources/daily/tasks/self/S7/BASE_COMMIT.txt +1 -0
- traceverdict/resources/daily/tasks/self/S7/repo.bundle +0 -0
- traceverdict/resources/daily/tasks/self/S7/task.yaml +22 -0
- traceverdict/resources/daily/tasks/self/S7/verify/README.md +1 -0
- traceverdict/resources/daily/tasks/self/S8/BASE_COMMIT.txt +1 -0
- traceverdict/resources/daily/tasks/self/S8/repo.bundle +0 -0
- traceverdict/resources/daily/tasks/self/S8/task.yaml +24 -0
- traceverdict/resources/daily/tasks/self/S8/verify/README.md +1 -0
- traceverdict/resources/daily/tasks/self/_image/Dockerfile +7 -0
- traceverdict/resources/daily/tasks/self/_image/SWEAgent.Dockerfile +10 -0
- traceverdict/resources/daily/tasks/self/_image/SWEAgentV2.Dockerfile +11 -0
- traceverdict/resources/daily/tasks/self/_image/SWEAgentV3.Dockerfile +10 -0
- traceverdict/resources/daily/tasks/self/_image/metadata.json +10 -0
- traceverdict/resources/daily/tasks/self/_image/requirements.txt +3 -0
- traceverdict/resources/daily/tasks/self/task_set.txt +8 -0
- traceverdict/resources.py +49 -0
- traceverdict/snapshot/__init__.py +1 -0
- traceverdict/snapshot/codex_image.py +143 -0
- traceverdict/snapshot/image.py +110 -0
- traceverdict/snapshot/patch.py +36 -0
- traceverdict/snapshot/suite_image.py +62 -0
- traceverdict/snapshot/workspace.py +113 -0
- traceverdict/swebench_adapter.py +508 -0
- traceverdict/swebench_budget.py +114 -0
- traceverdict/tracer/__init__.py +1 -0
- traceverdict/tracer/codex_jsonl.py +238 -0
- traceverdict/tracer/db.py +238 -0
- traceverdict/tracer/schema.sql +93 -0
- traceverdict/tracer/trajectory.py +422 -0
- traceverdict/verifier/__init__.py +213 -0
- traceverdict-0.2.0.dist-info/METADATA +123 -0
- traceverdict-0.2.0.dist-info/RECORD +89 -0
- traceverdict-0.2.0.dist-info/WHEEL +5 -0
- traceverdict-0.2.0.dist-info/entry_points.txt +3 -0
- traceverdict-0.2.0.dist-info/licenses/LICENSE +21 -0
- traceverdict-0.2.0.dist-info/top_level.txt +1 -0
traceverdict/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""adapters: integration layer for agents under test (mini_swe_agent first)."""
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Local-only Codex exec adapter for the M4-C compatibility arm (D24)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from traceverdict.adapters.mini_swe_agent import AdapterHarnessError, AdapterResult
|
|
13
|
+
from traceverdict.swebench_budget import ADAPTER_WALL_TIMEOUT_GRACE_S
|
|
14
|
+
|
|
15
|
+
PINNED_CODEX_VERSION = "0.144.4"
|
|
16
|
+
CODEX_TRAJECTORY_FORMAT = "codex-exec-jsonl-1"
|
|
17
|
+
CODEX_BINARY_IN_IMAGE = "/opt/traceverdict/codex"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _config_args(model_params: dict[str, Any]) -> list[str]:
|
|
21
|
+
"""Return the complete behavior-affecting Codex config overrides."""
|
|
22
|
+
effort = model_params.get("model_reasoning_effort")
|
|
23
|
+
if effort not in {"high", "medium"}:
|
|
24
|
+
raise AdapterHarnessError(
|
|
25
|
+
"model_reasoning_effort must be an explicitly approved M4-C value "
|
|
26
|
+
f"(high or medium), got {effort!r}"
|
|
27
|
+
)
|
|
28
|
+
expected = {
|
|
29
|
+
"auth_mode": "chatgpt_subscription",
|
|
30
|
+
"billing_mode": "subscription_unallocatable",
|
|
31
|
+
"approval_policy": "never",
|
|
32
|
+
"sandbox_mode": "workspace-write",
|
|
33
|
+
"web_search": "disabled",
|
|
34
|
+
"ephemeral": True,
|
|
35
|
+
"ignore_user_config": True,
|
|
36
|
+
"ignore_rules": True,
|
|
37
|
+
"concurrency": 1,
|
|
38
|
+
"network_mode": "bridge",
|
|
39
|
+
"container_cap_add": "SYS_ADMIN",
|
|
40
|
+
"container_seccomp": "unconfined",
|
|
41
|
+
"inner_sandbox": "workspace-write",
|
|
42
|
+
"auth_isolation": "per_tool_mount_namespace_hide_codex_home_v2",
|
|
43
|
+
}
|
|
44
|
+
mismatches = {
|
|
45
|
+
key: {"expected": value, "actual": model_params.get(key)}
|
|
46
|
+
for key, value in expected.items()
|
|
47
|
+
if model_params.get(key) != value
|
|
48
|
+
}
|
|
49
|
+
if mismatches:
|
|
50
|
+
raise AdapterHarnessError(
|
|
51
|
+
"immutable Codex M4-C identity mismatch: "
|
|
52
|
+
+ json.dumps(mismatches, sort_keys=True)
|
|
53
|
+
)
|
|
54
|
+
nested_expected = {
|
|
55
|
+
"sandbox_workspace_write": {"network_access": True},
|
|
56
|
+
"history": {"persistence": "none"},
|
|
57
|
+
"features": {
|
|
58
|
+
"shell_tool": True,
|
|
59
|
+
"shell_snapshot": False,
|
|
60
|
+
"unified_exec": False,
|
|
61
|
+
},
|
|
62
|
+
"hide_agent_reasoning": False,
|
|
63
|
+
}
|
|
64
|
+
nested_mismatches = {
|
|
65
|
+
key: {"expected": value, "actual": model_params.get(key)}
|
|
66
|
+
for key, value in nested_expected.items()
|
|
67
|
+
if model_params.get(key) != value
|
|
68
|
+
}
|
|
69
|
+
if nested_mismatches:
|
|
70
|
+
raise AdapterHarnessError(
|
|
71
|
+
"immutable Codex nested identity mismatch: "
|
|
72
|
+
+ json.dumps(nested_mismatches, sort_keys=True)
|
|
73
|
+
)
|
|
74
|
+
if model_params.get("service_tier") != "omitted":
|
|
75
|
+
raise AdapterHarnessError("service_tier must be explicitly omitted")
|
|
76
|
+
if model_params.get("temperature") != "omitted" or model_params.get("top_p") != "omitted":
|
|
77
|
+
raise AdapterHarnessError("temperature and top_p must be explicitly omitted")
|
|
78
|
+
return [
|
|
79
|
+
"-c", f"model_reasoning_effort=\"{effort}\"",
|
|
80
|
+
"-c", "approval_policy=\"never\"",
|
|
81
|
+
"-c", "web_search=\"disabled\"",
|
|
82
|
+
"-c", "history.persistence=\"none\"",
|
|
83
|
+
"-c", "hide_agent_reasoning=false",
|
|
84
|
+
"-c", "features.shell_tool=true",
|
|
85
|
+
"-c", "features.shell_snapshot=false",
|
|
86
|
+
"-c", "features.unified_exec=false",
|
|
87
|
+
"-c", "sandbox_workspace_write.network_access=true",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def build_codex_command(
|
|
92
|
+
*, model_name: str, model_params: dict[str, Any], container_cwd: str
|
|
93
|
+
) -> list[str]:
|
|
94
|
+
return [
|
|
95
|
+
CODEX_BINARY_IN_IMAGE,
|
|
96
|
+
"exec",
|
|
97
|
+
"--json",
|
|
98
|
+
"--ephemeral",
|
|
99
|
+
"--ignore-user-config",
|
|
100
|
+
"--ignore-rules",
|
|
101
|
+
"--strict-config",
|
|
102
|
+
"--model",
|
|
103
|
+
model_name,
|
|
104
|
+
"--sandbox",
|
|
105
|
+
"workspace-write",
|
|
106
|
+
"-C",
|
|
107
|
+
container_cwd,
|
|
108
|
+
*_config_args(model_params),
|
|
109
|
+
"-",
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def parse_codex_jsonl(data: str) -> list[dict[str, Any]]:
|
|
114
|
+
records: list[dict[str, Any]] = []
|
|
115
|
+
for line_number, raw in enumerate(data.splitlines(), start=1):
|
|
116
|
+
if not raw.strip():
|
|
117
|
+
continue
|
|
118
|
+
try:
|
|
119
|
+
item = json.loads(raw)
|
|
120
|
+
except json.JSONDecodeError as exc:
|
|
121
|
+
raise AdapterHarnessError(
|
|
122
|
+
f"Codex JSONL line {line_number} is invalid: {exc}"
|
|
123
|
+
) from exc
|
|
124
|
+
if not isinstance(item, dict) or not isinstance(item.get("type"), str):
|
|
125
|
+
raise AdapterHarnessError(
|
|
126
|
+
f"Codex JSONL line {line_number} lacks an event type"
|
|
127
|
+
)
|
|
128
|
+
records.append(item)
|
|
129
|
+
if not records:
|
|
130
|
+
raise AdapterHarnessError("Codex produced no JSONL records")
|
|
131
|
+
return records
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def run_codex(
|
|
135
|
+
*,
|
|
136
|
+
instruction: str,
|
|
137
|
+
image: str,
|
|
138
|
+
docker_executable: str,
|
|
139
|
+
host_work_path: Path,
|
|
140
|
+
container_cwd: str,
|
|
141
|
+
model_name: str,
|
|
142
|
+
model_params: dict[str, Any],
|
|
143
|
+
litellm_model_registry: Path | None,
|
|
144
|
+
agent_version: str,
|
|
145
|
+
cost_limit: float,
|
|
146
|
+
step_limit: int,
|
|
147
|
+
wall_time_s: int,
|
|
148
|
+
work_dir: Path,
|
|
149
|
+
) -> AdapterResult:
|
|
150
|
+
"""Run Codex inside the local agent-layer image; never use a remote host."""
|
|
151
|
+
del litellm_model_registry, cost_limit, step_limit
|
|
152
|
+
if agent_version != PINNED_CODEX_VERSION:
|
|
153
|
+
raise AdapterHarnessError(
|
|
154
|
+
f"Codex version mismatch: expected {PINNED_CODEX_VERSION}, got {agent_version}"
|
|
155
|
+
)
|
|
156
|
+
auth_spec = os.environ.get("TRACEVERDICT_CODEX_AUTH_FILE")
|
|
157
|
+
if not auth_spec:
|
|
158
|
+
raise AdapterHarnessError("TRACEVERDICT_CODEX_AUTH_FILE is required locally")
|
|
159
|
+
auth_file = Path(auth_spec).resolve()
|
|
160
|
+
if not auth_file.is_file():
|
|
161
|
+
raise AdapterHarnessError("local Codex auth file does not exist")
|
|
162
|
+
work_dir.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
jsonl_path = work_dir / "codex.events.jsonl"
|
|
164
|
+
stderr_path = work_dir / "codex.stderr.log"
|
|
165
|
+
command = build_codex_command(
|
|
166
|
+
model_name=model_name,
|
|
167
|
+
model_params=model_params,
|
|
168
|
+
container_cwd=container_cwd,
|
|
169
|
+
)
|
|
170
|
+
host_work = str(host_work_path.resolve()).replace("\\", "/")
|
|
171
|
+
host_auth = str(auth_file).replace("\\", "/")
|
|
172
|
+
docker_command = [
|
|
173
|
+
docker_executable,
|
|
174
|
+
"run",
|
|
175
|
+
"--rm",
|
|
176
|
+
"-i",
|
|
177
|
+
"--network",
|
|
178
|
+
"bridge",
|
|
179
|
+
"--cap-add",
|
|
180
|
+
"SYS_ADMIN",
|
|
181
|
+
"--security-opt",
|
|
182
|
+
"seccomp=unconfined",
|
|
183
|
+
"-e",
|
|
184
|
+
"CODEX_HOME=/run/traceverdict-codex",
|
|
185
|
+
"-v",
|
|
186
|
+
f"{host_work}:{container_cwd}",
|
|
187
|
+
"-v",
|
|
188
|
+
f"{host_auth}:/run/traceverdict-codex/auth.json",
|
|
189
|
+
"-w",
|
|
190
|
+
container_cwd,
|
|
191
|
+
image,
|
|
192
|
+
*command,
|
|
193
|
+
]
|
|
194
|
+
timeout = max(1, int(wall_time_s)) + ADAPTER_WALL_TIMEOUT_GRACE_S
|
|
195
|
+
try:
|
|
196
|
+
proc = subprocess.run(
|
|
197
|
+
docker_command,
|
|
198
|
+
input=instruction,
|
|
199
|
+
capture_output=True,
|
|
200
|
+
text=True,
|
|
201
|
+
encoding="utf-8",
|
|
202
|
+
errors="strict",
|
|
203
|
+
timeout=timeout,
|
|
204
|
+
check=False,
|
|
205
|
+
)
|
|
206
|
+
except subprocess.TimeoutExpired as exc:
|
|
207
|
+
raise AdapterHarnessError(
|
|
208
|
+
f"Codex exceeded adapter timeout {timeout}s", exit_reason="TimeExceeded"
|
|
209
|
+
) from exc
|
|
210
|
+
jsonl_path.write_text(proc.stdout or "", encoding="utf-8")
|
|
211
|
+
stderr_path.write_text(proc.stderr or "", encoding="utf-8")
|
|
212
|
+
records = parse_codex_jsonl(proc.stdout or "")
|
|
213
|
+
trajectory = {
|
|
214
|
+
"trajectory_format": CODEX_TRAJECTORY_FORMAT,
|
|
215
|
+
"records": records,
|
|
216
|
+
"jsonl_sha256": hashlib.sha256((proc.stdout or "").encode("utf-8")).hexdigest(),
|
|
217
|
+
"instruction_sha256": hashlib.sha256(instruction.encode("utf-8")).hexdigest(),
|
|
218
|
+
"info": {
|
|
219
|
+
"submission": "",
|
|
220
|
+
"model_stats": {"instance_cost": None},
|
|
221
|
+
"returncode": proc.returncode,
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
trajectory_path = work_dir / "codex.trajectory.json"
|
|
225
|
+
trajectory_path.write_text(
|
|
226
|
+
json.dumps(trajectory, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
227
|
+
)
|
|
228
|
+
return AdapterResult(
|
|
229
|
+
traj=trajectory,
|
|
230
|
+
traj_path=trajectory_path,
|
|
231
|
+
returncode=proc.returncode,
|
|
232
|
+
stdout=proc.stdout or "",
|
|
233
|
+
stderr=proc.stderr or "",
|
|
234
|
+
source_artifacts={"codex_jsonl": jsonl_path},
|
|
235
|
+
)
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""Drive mini-swe-agent via subprocess with DockerEnvironment (D1-a/d/f)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from importlib import metadata
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from traceverdict.core.simple_yaml import dump_to_path
|
|
18
|
+
from traceverdict.swebench_budget import (
|
|
19
|
+
ADAPTER_WALL_TIMEOUT_GRACE_S,
|
|
20
|
+
AGENT_TOOL_TIMEOUT_S,
|
|
21
|
+
)
|
|
22
|
+
from traceverdict.tracer.trajectory import TrajectoryFormatError, assert_trajectory_format
|
|
23
|
+
|
|
24
|
+
PINNED_MINI_SWE_AGENT = "2.4.5"
|
|
25
|
+
|
|
26
|
+
# mini-swe-agent 2.4.5 creates prompt_toolkit PromptSession objects at import
|
|
27
|
+
# time, even for yolo/non-interactive runs. On Windows a subprocess whose
|
|
28
|
+
# stdout is captured has no console screen buffer, so that import otherwise
|
|
29
|
+
# raises NoConsoleScreenBufferError before the CLI can parse ``-y``. Running
|
|
30
|
+
# the module inside a DummyOutput app session keeps the process headless while
|
|
31
|
+
# preserving stdout/stderr capture for harness diagnostics.
|
|
32
|
+
_WINDOWS_HEADLESS_WRAPPER = (
|
|
33
|
+
"from prompt_toolkit.application.current import create_app_session;"
|
|
34
|
+
"from prompt_toolkit.output import DummyOutput;"
|
|
35
|
+
"import runpy;"
|
|
36
|
+
"ctx=create_app_session(output=DummyOutput());"
|
|
37
|
+
"ctx.__enter__();"
|
|
38
|
+
"runpy.run_module('minisweagent.run.mini',run_name='__main__')"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AdapterHarnessError(RuntimeError):
|
|
43
|
+
"""Harness-level failure before/around agent execution."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, message: str, *, exit_reason: str = "harness_error"):
|
|
46
|
+
super().__init__(message)
|
|
47
|
+
self.exit_reason = exit_reason
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def installed_mini_swe_agent_version() -> str | None:
|
|
51
|
+
"""Return installed mini-swe-agent version or None if not installed."""
|
|
52
|
+
try:
|
|
53
|
+
return metadata.version("mini-swe-agent")
|
|
54
|
+
except metadata.PackageNotFoundError:
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def assert_agent_version(expected: str) -> str:
|
|
59
|
+
"""D1-a: installed version must match config.agent_version."""
|
|
60
|
+
got = installed_mini_swe_agent_version()
|
|
61
|
+
if got is None:
|
|
62
|
+
raise AdapterHarnessError(
|
|
63
|
+
f"mini-swe-agent not installed; require =={expected} (D1-a)",
|
|
64
|
+
exit_reason="harness_error",
|
|
65
|
+
)
|
|
66
|
+
if got != expected:
|
|
67
|
+
raise AdapterHarnessError(
|
|
68
|
+
f"mini-swe-agent version mismatch: installed={got!r} "
|
|
69
|
+
f"config.agent_version={expected!r} (D1-a)",
|
|
70
|
+
exit_reason="harness_error",
|
|
71
|
+
)
|
|
72
|
+
return got
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def find_mini_cli() -> str:
|
|
76
|
+
for name in ("mini-swe-agent", "mini"):
|
|
77
|
+
path = shutil.which(name)
|
|
78
|
+
if path:
|
|
79
|
+
return path
|
|
80
|
+
# Fallback: python -m minisweagent
|
|
81
|
+
return ""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def build_mini_command(cli: str, args: list[str]) -> list[str]:
|
|
85
|
+
"""Build a non-interactive mini-swe-agent command for this platform."""
|
|
86
|
+
if os.name == "nt":
|
|
87
|
+
return [sys.executable, "-c", _WINDOWS_HEADLESS_WRAPPER, *args]
|
|
88
|
+
if cli:
|
|
89
|
+
return [cli, *args]
|
|
90
|
+
return [sys.executable, "-m", "minisweagent.run.mini", *args]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class AdapterResult:
|
|
95
|
+
traj: dict[str, Any]
|
|
96
|
+
traj_path: Path
|
|
97
|
+
returncode: int
|
|
98
|
+
stdout: str
|
|
99
|
+
stderr: str
|
|
100
|
+
source_artifacts: dict[str, Path] = field(default_factory=dict)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _build_mini_config(
|
|
104
|
+
*,
|
|
105
|
+
image: str,
|
|
106
|
+
docker_executable: str,
|
|
107
|
+
host_work_path: Path,
|
|
108
|
+
container_cwd: str,
|
|
109
|
+
model_name: str,
|
|
110
|
+
model_params: dict[str, Any],
|
|
111
|
+
litellm_model_registry: Path,
|
|
112
|
+
output_path: Path,
|
|
113
|
+
cost_limit: float,
|
|
114
|
+
step_limit: int,
|
|
115
|
+
wall_time_s: int,
|
|
116
|
+
) -> dict[str, Any]:
|
|
117
|
+
"""YAML config for mini-swe-agent: DockerEnvironment + litellm model + default agent."""
|
|
118
|
+
# Windows paths for docker -v need absolute path; docker desktop accepts forward slashes.
|
|
119
|
+
host = str(host_work_path.resolve()).replace("\\", "/")
|
|
120
|
+
run_args = [
|
|
121
|
+
"--rm",
|
|
122
|
+
"-v",
|
|
123
|
+
f"{host}:{container_cwd}",
|
|
124
|
+
]
|
|
125
|
+
raw_model_params = dict(model_params or {})
|
|
126
|
+
injection = dict(raw_model_params.pop("_traceverdict_injection", {}) or {})
|
|
127
|
+
prompt_override = raw_model_params.pop("_traceverdict_system_prompt", None)
|
|
128
|
+
injection_id = str(injection.get("id", "")).upper()
|
|
129
|
+
if injection_id == "I3Q":
|
|
130
|
+
run_args[-1] = f"{host}:{container_cwd}:ro"
|
|
131
|
+
system_template = (
|
|
132
|
+
"You are a helpful assistant that can interact with a computer shell "
|
|
133
|
+
"to solve programming tasks."
|
|
134
|
+
)
|
|
135
|
+
if prompt_override is not None:
|
|
136
|
+
if not isinstance(prompt_override, dict):
|
|
137
|
+
raise AdapterHarnessError("daily system prompt identity must be a mapping")
|
|
138
|
+
content = prompt_override.get("content")
|
|
139
|
+
expected_sha = prompt_override.get("sha256")
|
|
140
|
+
if not isinstance(content, str) or not content.strip():
|
|
141
|
+
raise AdapterHarnessError("daily system prompt content is empty")
|
|
142
|
+
actual_sha = hashlib.sha256(content.encode("utf-8")).hexdigest()
|
|
143
|
+
if expected_sha != actual_sha:
|
|
144
|
+
raise AdapterHarnessError("daily system prompt SHA256 mismatch")
|
|
145
|
+
system_template = content
|
|
146
|
+
tool_instruction = "You can execute bash commands and edit files.\n"
|
|
147
|
+
instance_template = (
|
|
148
|
+
"Please solve this issue in {{cwd}}:\n{{task}}\n\n"
|
|
149
|
+
+ tool_instruction
|
|
150
|
+
+ "When done, submit with: echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT\n"
|
|
151
|
+
"Optionally print a patch after that line."
|
|
152
|
+
)
|
|
153
|
+
if injection_id == "I1":
|
|
154
|
+
instance_template = instance_template.replace(tool_instruction, "")
|
|
155
|
+
elif injection_id == "I1P":
|
|
156
|
+
instance_template = (
|
|
157
|
+
"Please work in {{cwd}}.\n\n"
|
|
158
|
+
+ tool_instruction
|
|
159
|
+
+ "When done, submit with: echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT\n"
|
|
160
|
+
"Optionally print a patch after that line."
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
agent: dict[str, Any] = {
|
|
164
|
+
"agent_class": "default",
|
|
165
|
+
"output_path": str(output_path),
|
|
166
|
+
"cost_limit": cost_limit,
|
|
167
|
+
"step_limit": step_limit if step_limit > 0 else 0,
|
|
168
|
+
"system_template": system_template,
|
|
169
|
+
"instance_template": instance_template,
|
|
170
|
+
}
|
|
171
|
+
if wall_time_s and wall_time_s > 0:
|
|
172
|
+
agent["wall_time_limit_seconds"] = wall_time_s
|
|
173
|
+
|
|
174
|
+
model_kwargs = raw_model_params
|
|
175
|
+
# The OpenAI SDK only accepts DeepSeek's vendor-specific thinking switch
|
|
176
|
+
# through extra_body. This transport normalization is deliberately outside
|
|
177
|
+
# immutable experiment config identity (D4-a).
|
|
178
|
+
if "thinking" in model_kwargs:
|
|
179
|
+
thinking = model_kwargs.pop("thinking")
|
|
180
|
+
extra_body = dict(model_kwargs.get("extra_body") or {})
|
|
181
|
+
extra_body["thinking"] = thinking
|
|
182
|
+
model_kwargs["extra_body"] = extra_body
|
|
183
|
+
|
|
184
|
+
observation_template = (
|
|
185
|
+
"{% if output.exception_info %}<exception>{{output.exception_info}}</exception>\n{% endif %}"
|
|
186
|
+
"<returncode>{{output.returncode}}</returncode>\n<output>\n"
|
|
187
|
+
+ (
|
|
188
|
+
"{{ output.output[:500] }}"
|
|
189
|
+
if injection_id == "I2"
|
|
190
|
+
else "" if injection_id == "I2P" else "{{ output.output }}"
|
|
191
|
+
)
|
|
192
|
+
+ "</output>"
|
|
193
|
+
)
|
|
194
|
+
interpreter = ["bash", "-c"]
|
|
195
|
+
if injection_id == "I3":
|
|
196
|
+
guard = (
|
|
197
|
+
"case \"$1\" in *pytest*) echo 'pytest disabled by TraceVerdict I3' >&2; exit 126;; "
|
|
198
|
+
"*) exec bash -c \"$1\";; esac"
|
|
199
|
+
)
|
|
200
|
+
interpreter = ["bash", "-c", guard, "traceverdict-i3"]
|
|
201
|
+
|
|
202
|
+
model_classes = {
|
|
203
|
+
"I1Q": "traceverdict.injections.BrokenToolNameLitellmModel",
|
|
204
|
+
"I5": "traceverdict.injections.HistoryWindowLitellmModel",
|
|
205
|
+
"I5P": "traceverdict.injections.HistoryResetLitellmModel",
|
|
206
|
+
}
|
|
207
|
+
model: dict[str, Any] = {
|
|
208
|
+
"model_class": model_classes.get(injection_id, "litellm"),
|
|
209
|
+
"model_name": model_name,
|
|
210
|
+
"model_kwargs": model_kwargs,
|
|
211
|
+
"litellm_model_registry": str(litellm_model_registry.resolve()),
|
|
212
|
+
"cost_tracking": "default",
|
|
213
|
+
"observation_template": observation_template,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
"agent": agent,
|
|
218
|
+
"environment": {
|
|
219
|
+
"environment_class": "docker",
|
|
220
|
+
# D1-d: use the exact CLI already validated by require_docker().
|
|
221
|
+
# This is essential for per-user Docker Desktop installs on
|
|
222
|
+
# Windows, where a nested Python process may see a stale PATH.
|
|
223
|
+
"executable": docker_executable,
|
|
224
|
+
"image": image,
|
|
225
|
+
"cwd": container_cwd,
|
|
226
|
+
"timeout": AGENT_TOOL_TIMEOUT_S,
|
|
227
|
+
"interpreter": interpreter,
|
|
228
|
+
"run_args": run_args,
|
|
229
|
+
"env": {
|
|
230
|
+
"PAGER": "cat",
|
|
231
|
+
"PIP_PROGRESS_BAR": "off",
|
|
232
|
+
# Test execution must not pollute the authoritative patch
|
|
233
|
+
# with transient __pycache__ files from the disposable repo.
|
|
234
|
+
"PYTHONDONTWRITEBYTECODE": "enabled",
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
"model": model,
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def run_mini_swe_agent(
|
|
242
|
+
*,
|
|
243
|
+
instruction: str,
|
|
244
|
+
image: str,
|
|
245
|
+
docker_executable: str,
|
|
246
|
+
host_work_path: Path,
|
|
247
|
+
container_cwd: str,
|
|
248
|
+
model_name: str,
|
|
249
|
+
model_params: dict[str, Any],
|
|
250
|
+
litellm_model_registry: Path,
|
|
251
|
+
agent_version: str,
|
|
252
|
+
cost_limit: float,
|
|
253
|
+
step_limit: int,
|
|
254
|
+
wall_time_s: int,
|
|
255
|
+
work_dir: Path | None = None,
|
|
256
|
+
) -> AdapterResult:
|
|
257
|
+
"""Subprocess-run mini-swe-agent against DockerEnvironment mounting host_work_path.
|
|
258
|
+
|
|
259
|
+
Strict Docker only (D1-d). Does not import agent for execution; uses CLI.
|
|
260
|
+
"""
|
|
261
|
+
assert_agent_version(agent_version)
|
|
262
|
+
|
|
263
|
+
work_dir = Path(work_dir) if work_dir else Path(tempfile.mkdtemp(prefix="traceverdict-mini-"))
|
|
264
|
+
work_dir.mkdir(parents=True, exist_ok=True)
|
|
265
|
+
traj_path = work_dir / "run.traj.json"
|
|
266
|
+
cfg_path = work_dir / "mini_config.yaml"
|
|
267
|
+
cfg = _build_mini_config(
|
|
268
|
+
image=image,
|
|
269
|
+
docker_executable=docker_executable,
|
|
270
|
+
host_work_path=host_work_path,
|
|
271
|
+
container_cwd=container_cwd,
|
|
272
|
+
model_name=model_name,
|
|
273
|
+
model_params=model_params,
|
|
274
|
+
litellm_model_registry=litellm_model_registry,
|
|
275
|
+
output_path=traj_path,
|
|
276
|
+
cost_limit=cost_limit,
|
|
277
|
+
step_limit=step_limit,
|
|
278
|
+
wall_time_s=wall_time_s,
|
|
279
|
+
)
|
|
280
|
+
dump_to_path(cfg_path, cfg)
|
|
281
|
+
injection_id = str(
|
|
282
|
+
((model_params or {}).get("_traceverdict_injection") or {}).get("id", "")
|
|
283
|
+
).upper()
|
|
284
|
+
model_class_arg = {
|
|
285
|
+
"I1Q": "traceverdict.injections.BrokenToolNameLitellmModel",
|
|
286
|
+
"I5": "traceverdict.injections.HistoryWindowLitellmModel",
|
|
287
|
+
"I5P": "traceverdict.injections.HistoryResetLitellmModel",
|
|
288
|
+
}.get(injection_id, "litellm")
|
|
289
|
+
|
|
290
|
+
args = [
|
|
291
|
+
"-c",
|
|
292
|
+
str(cfg_path),
|
|
293
|
+
"-t",
|
|
294
|
+
instruction,
|
|
295
|
+
"-o",
|
|
296
|
+
str(traj_path),
|
|
297
|
+
"-y",
|
|
298
|
+
"--exit-immediately",
|
|
299
|
+
"--agent-class",
|
|
300
|
+
"default",
|
|
301
|
+
"--environment-class",
|
|
302
|
+
"docker",
|
|
303
|
+
"--model-class",
|
|
304
|
+
model_class_arg,
|
|
305
|
+
"-m",
|
|
306
|
+
model_name,
|
|
307
|
+
]
|
|
308
|
+
cmd = build_mini_command(find_mini_cli(), args)
|
|
309
|
+
|
|
310
|
+
env = os.environ.copy()
|
|
311
|
+
env["MSWEA_SILENT_STARTUP"] = "1"
|
|
312
|
+
# The adapter supplies model/task/config explicitly and must never enter
|
|
313
|
+
# mini-swe-agent's first-run API-key wizard in a harness subprocess.
|
|
314
|
+
env["MSWEA_CONFIGURED"] = "1"
|
|
315
|
+
|
|
316
|
+
proc = subprocess.run(
|
|
317
|
+
cmd,
|
|
318
|
+
capture_output=True,
|
|
319
|
+
text=True,
|
|
320
|
+
env=env,
|
|
321
|
+
timeout=(
|
|
322
|
+
max(wall_time_s + ADAPTER_WALL_TIMEOUT_GRACE_S, 300)
|
|
323
|
+
if wall_time_s
|
|
324
|
+
else 3600
|
|
325
|
+
),
|
|
326
|
+
check=False,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Preserve the complete child-process diagnostics before parsing its
|
|
330
|
+
# trajectory. Rich tracebacks put the root exception at the end, so a
|
|
331
|
+
# prefix-only error message can hide the actionable failure entirely.
|
|
332
|
+
stdout_path = work_dir / "mini.stdout.log"
|
|
333
|
+
stderr_path = work_dir / "mini.stderr.log"
|
|
334
|
+
stdout_path.write_text(proc.stdout or "", encoding="utf-8")
|
|
335
|
+
stderr_path.write_text(proc.stderr or "", encoding="utf-8")
|
|
336
|
+
|
|
337
|
+
if not traj_path.is_file():
|
|
338
|
+
raise AdapterHarnessError(
|
|
339
|
+
"mini-swe-agent produced no trajectory file. "
|
|
340
|
+
f"returncode={proc.returncode} "
|
|
341
|
+
f"stderr_tail={(proc.stderr or '')[-4000:]!r} "
|
|
342
|
+
f"stdout_tail={(proc.stdout or '')[-2000:]!r}; "
|
|
343
|
+
f"full logs: {stderr_path}, {stdout_path}",
|
|
344
|
+
exit_reason="harness_error",
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
traj = json.loads(traj_path.read_text(encoding="utf-8"))
|
|
349
|
+
except json.JSONDecodeError as e:
|
|
350
|
+
raise AdapterHarnessError(
|
|
351
|
+
f"invalid trajectory JSON: {e}", exit_reason="harness_error"
|
|
352
|
+
) from e
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
assert_trajectory_format(traj)
|
|
356
|
+
except TrajectoryFormatError as e:
|
|
357
|
+
raise AdapterHarnessError(str(e), exit_reason="harness_error") from e
|
|
358
|
+
|
|
359
|
+
return AdapterResult(
|
|
360
|
+
traj=traj,
|
|
361
|
+
traj_path=traj_path,
|
|
362
|
+
returncode=proc.returncode,
|
|
363
|
+
stdout=proc.stdout,
|
|
364
|
+
stderr=proc.stderr,
|
|
365
|
+
)
|