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.
Files changed (89) hide show
  1. traceverdict/__init__.py +3 -0
  2. traceverdict/adapters/__init__.py +1 -0
  3. traceverdict/adapters/codex.py +235 -0
  4. traceverdict/adapters/mini_swe_agent.py +365 -0
  5. traceverdict/adapters/swe_agent.py +437 -0
  6. traceverdict/adapters/swe_agent_entrypoint.py +119 -0
  7. traceverdict/cli.py +309 -0
  8. traceverdict/compare/__init__.py +314 -0
  9. traceverdict/compare/constants.py +12 -0
  10. traceverdict/core/__init__.py +1 -0
  11. traceverdict/core/config_loader.py +72 -0
  12. traceverdict/core/runner.py +570 -0
  13. traceverdict/core/selftest.py +393 -0
  14. traceverdict/core/simple_yaml.py +252 -0
  15. traceverdict/core/suite.py +77 -0
  16. traceverdict/core/task_loader.py +69 -0
  17. traceverdict/cost_ledger.py +47 -0
  18. traceverdict/daily.py +397 -0
  19. traceverdict/ingest.py +272 -0
  20. traceverdict/injections/__init__.py +134 -0
  21. traceverdict/m3.py +484 -0
  22. traceverdict/m4c.py +134 -0
  23. traceverdict/m4d.py +381 -0
  24. traceverdict/m4s.py +154 -0
  25. traceverdict/report/__init__.py +128 -0
  26. traceverdict/report/taxonomy.py +76 -0
  27. traceverdict/resources/daily/configs/dev.yaml +15 -0
  28. traceverdict/resources/daily/configs/litellm_models.json +18 -0
  29. traceverdict/resources/daily/configs/litellm_models.meta.json +30 -0
  30. traceverdict/resources/daily/tasks/self/S1/BASE_COMMIT.txt +1 -0
  31. traceverdict/resources/daily/tasks/self/S1/repo.bundle +0 -0
  32. traceverdict/resources/daily/tasks/self/S1/task.yaml +26 -0
  33. traceverdict/resources/daily/tasks/self/S1/verify/README.md +11 -0
  34. traceverdict/resources/daily/tasks/self/S2/BASE_COMMIT.txt +1 -0
  35. traceverdict/resources/daily/tasks/self/S2/repo.bundle +0 -0
  36. traceverdict/resources/daily/tasks/self/S2/task.yaml +24 -0
  37. traceverdict/resources/daily/tasks/self/S2/verify/README.md +1 -0
  38. traceverdict/resources/daily/tasks/self/S3/BASE_COMMIT.txt +1 -0
  39. traceverdict/resources/daily/tasks/self/S3/repo.bundle +0 -0
  40. traceverdict/resources/daily/tasks/self/S3/task.yaml +24 -0
  41. traceverdict/resources/daily/tasks/self/S3/verify/README.md +1 -0
  42. traceverdict/resources/daily/tasks/self/S4/BASE_COMMIT.txt +1 -0
  43. traceverdict/resources/daily/tasks/self/S4/repo.bundle +0 -0
  44. traceverdict/resources/daily/tasks/self/S4/task.yaml +24 -0
  45. traceverdict/resources/daily/tasks/self/S4/verify/README.md +1 -0
  46. traceverdict/resources/daily/tasks/self/S5/BASE_COMMIT.txt +1 -0
  47. traceverdict/resources/daily/tasks/self/S5/repo.bundle +0 -0
  48. traceverdict/resources/daily/tasks/self/S5/task.yaml +26 -0
  49. traceverdict/resources/daily/tasks/self/S5/verify/README.md +1 -0
  50. traceverdict/resources/daily/tasks/self/S6/BASE_COMMIT.txt +1 -0
  51. traceverdict/resources/daily/tasks/self/S6/repo.bundle +0 -0
  52. traceverdict/resources/daily/tasks/self/S6/task.yaml +29 -0
  53. traceverdict/resources/daily/tasks/self/S6/verify/README.md +3 -0
  54. traceverdict/resources/daily/tasks/self/S7/BASE_COMMIT.txt +1 -0
  55. traceverdict/resources/daily/tasks/self/S7/repo.bundle +0 -0
  56. traceverdict/resources/daily/tasks/self/S7/task.yaml +22 -0
  57. traceverdict/resources/daily/tasks/self/S7/verify/README.md +1 -0
  58. traceverdict/resources/daily/tasks/self/S8/BASE_COMMIT.txt +1 -0
  59. traceverdict/resources/daily/tasks/self/S8/repo.bundle +0 -0
  60. traceverdict/resources/daily/tasks/self/S8/task.yaml +24 -0
  61. traceverdict/resources/daily/tasks/self/S8/verify/README.md +1 -0
  62. traceverdict/resources/daily/tasks/self/_image/Dockerfile +7 -0
  63. traceverdict/resources/daily/tasks/self/_image/SWEAgent.Dockerfile +10 -0
  64. traceverdict/resources/daily/tasks/self/_image/SWEAgentV2.Dockerfile +11 -0
  65. traceverdict/resources/daily/tasks/self/_image/SWEAgentV3.Dockerfile +10 -0
  66. traceverdict/resources/daily/tasks/self/_image/metadata.json +10 -0
  67. traceverdict/resources/daily/tasks/self/_image/requirements.txt +3 -0
  68. traceverdict/resources/daily/tasks/self/task_set.txt +8 -0
  69. traceverdict/resources.py +49 -0
  70. traceverdict/snapshot/__init__.py +1 -0
  71. traceverdict/snapshot/codex_image.py +143 -0
  72. traceverdict/snapshot/image.py +110 -0
  73. traceverdict/snapshot/patch.py +36 -0
  74. traceverdict/snapshot/suite_image.py +62 -0
  75. traceverdict/snapshot/workspace.py +113 -0
  76. traceverdict/swebench_adapter.py +508 -0
  77. traceverdict/swebench_budget.py +114 -0
  78. traceverdict/tracer/__init__.py +1 -0
  79. traceverdict/tracer/codex_jsonl.py +238 -0
  80. traceverdict/tracer/db.py +238 -0
  81. traceverdict/tracer/schema.sql +93 -0
  82. traceverdict/tracer/trajectory.py +422 -0
  83. traceverdict/verifier/__init__.py +213 -0
  84. traceverdict-0.2.0.dist-info/METADATA +123 -0
  85. traceverdict-0.2.0.dist-info/RECORD +89 -0
  86. traceverdict-0.2.0.dist-info/WHEEL +5 -0
  87. traceverdict-0.2.0.dist-info/entry_points.txt +3 -0
  88. traceverdict-0.2.0.dist-info/licenses/LICENSE +21 -0
  89. traceverdict-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3 @@
1
+ """TraceVerdict: stateful regression evaluation system for coding agents."""
2
+
3
+ __version__ = "0.2.0"
@@ -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
+ )