Skip to content

Commit c7e029e

Browse files
committed
migrate to mini version > v2
1 parent 87053b2 commit c7e029e

14 files changed

Lines changed: 496 additions & 497 deletions

File tree

.env.example

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
# Required: GitHub token with repo access for cloning game repositories
55
GITHUB_TOKEN=your_github_token_here
66

7-
# Optional: LLM Provider API Keys (configure the ones you plan to use)
8-
OPENAI_API_KEY=
7+
# LLM provider API keys — set the ones for the models you run (see configs/models.yaml).
8+
# Models are resolved by litellm from their provider-prefixed names.
99
ANTHROPIC_API_KEY=
10+
OPENAI_API_KEY=
11+
GEMINI_API_KEY=
12+
XAI_API_KEY=
13+
DASHSCOPE_API_KEY=

codeclash/agents/minisweagent.py

Lines changed: 14 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@
33
import traceback
44
from collections.abc import Callable
55

6-
from minisweagent import Model
76
from minisweagent.agents.default import AgentConfig, DefaultAgent
87
from minisweagent.environments.docker import DockerEnvironment
9-
from minisweagent.models import get_model
10-
from minisweagent.models.test_models import DeterministicModel
11-
from minisweagent.run.utils.save import save_traj
8+
from minisweagent.models import Model, get_model
129

1310
from codeclash import REPO_DIR
1411
from codeclash.agents.player import Player
@@ -22,10 +19,8 @@
2219

2320

2421
class ClashAgent(DefaultAgent):
25-
"""
26-
Slightly modified version of `DefaultAgent` from mini-SWE-agent
27-
(https://github.com/SWE-agent/mini-swe-agent)
28-
"""
22+
"""`DefaultAgent` from mini-SWE-agent (https://github.com/SWE-agent/mini-swe-agent)
23+
with per-player debug logging."""
2924

3025
def __init__(
3126
self,
@@ -39,9 +34,11 @@ def __init__(
3934
super().__init__(model, env, config_class=config_class, **kwargs)
4035
self.logger = logger
4136

42-
def add_message(self, role: str, content: str, **kwargs):
43-
super().add_message(role, content, **kwargs)
44-
self.logger.debug(f"[{role}] {content}", extra={"highlighter": None})
37+
def add_messages(self, *messages: dict) -> list[dict]:
38+
result = super().add_messages(*messages)
39+
for m in messages:
40+
self.logger.debug(f"[{m.get('role')}] {m.get('content')}", extra={"highlighter": None})
41+
return result
4542

4643

4744
class MiniSWEAgent(Player):
@@ -51,26 +48,21 @@ def __init__(self, config: dict, environment: DockerEnvironment, game_context: G
5148
super().__init__(config, environment=environment, game_context=game_context)
5249

5350
def run(self):
54-
# temporary workaround around https://github.com/SWE-agent/mini-swe-agent/issues/477
55-
if "DeterministicModel" not in self.config["config"]["model"].get("model_class", ""):
56-
model = get_model(config=self.config["config"]["model"])
57-
else:
58-
model = DeterministicModel(outputs=self.config["config"]["model"]["outputs"])
51+
model = get_model(config=self.config["config"]["model"])
5952
self.agent = ClashAgent(
6053
model=model,
6154
env=self.environment,
6255
logger=self.logger,
6356
**self.config["config"]["agent"],
6457
)
6558
exit_status = None
66-
result = None
6759
exc_message = None
6860
try:
69-
exit_status, result = self.agent.run(task="", **self.game_context.to_template_vars())
61+
result = self.agent.run(task="", **self.game_context.to_template_vars())
62+
exit_status = result.get("exit_status", "")
7063
except Exception as e:
7164
exit_status = str(e)
7265
exc_message = traceback.format_exc()
73-
result = exc_message
7466
self.logger.critical(exc_message)
7567
finally:
7668
traj_path = (
@@ -79,22 +71,16 @@ def run(self):
7971
/ self.name
8072
/ f"{self.name}_r{self.game_context.round}.traj.json"
8173
)
82-
save_traj(
83-
self.agent, # type: ignore
84-
traj_path,
85-
exit_status=exit_status,
86-
result=result,
87-
print_fct=self.logger.debug,
88-
)
74+
self.agent.save(traj_path)
8975
copy_to_container(
9076
self.environment,
9177
traj_path,
9278
self.game_context.log_env / "edits" / traj_path.name,
9379
)
9480
self._metadata["agent_stats"][self.game_context.round] = {
9581
"exit_status": exit_status,
96-
"cost": self.agent.model.cost,
97-
"api_calls": self.agent.model.n_calls,
82+
"cost": self.agent.cost,
83+
"api_calls": self.agent.n_calls,
9884
}
9985
if exit_status.lower().strip() not in ["", "submitted", "limitsexceeded"] and exc_message is not None:
10086
raise RuntimeError(f"Agent {self.name} failed with exit status: {exit_status} and exception: {exc_message}")

codeclash/arenas/arena.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
from codeclash.agents.player import Player
1313
from codeclash.constants import DIR_LOGS, DIR_WORK, GH_ORG, RESULT_TIE
1414
from codeclash.utils.aws import is_running_in_aws_batch, pull_game_container_aws_ecr
15-
from codeclash.utils.environment import assert_zero_exit_code, copy_between_containers, copy_from_container
15+
from codeclash.utils.environment import (
16+
ClashDockerEnvironment,
17+
assert_zero_exit_code,
18+
copy_between_containers,
19+
copy_from_container,
20+
)
1621
from codeclash.utils.log import get_logger
1722

1823

@@ -185,7 +190,7 @@ def get_environment(self, branch_name: str | None = None) -> DockerEnvironment:
185190
run_args = ["--rm"]
186191
else:
187192
run_args = []
188-
environment = DockerEnvironment(
193+
environment = ClashDockerEnvironment(
189194
image=self.image_name,
190195
cwd=str(DIR_WORK),
191196
env={

codeclash/utils/environment.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import os
23
import shutil
34
import subprocess
45
import tempfile
@@ -10,6 +11,28 @@
1011
COPY_EXCLUDE_PATTERNS = [".git", "__pycache__"]
1112

1213

14+
def _scratch_dir() -> str | None:
15+
"""Local scratch dir for staging `docker cp` transfers. Defaults to the system temp dir.
16+
Override with CODECLASH_TMPDIR (e.g. on AWS Batch, where the default temp dir misbehaves)."""
17+
override = os.getenv("CODECLASH_TMPDIR")
18+
if override:
19+
Path(override).mkdir(parents=True, exist_ok=True)
20+
return override
21+
22+
23+
class ClashDockerEnvironment(DockerEnvironment):
24+
"""DockerEnvironment that also accepts a plain command string.
25+
26+
mini-swe-agent v2's `execute` takes an action dict (`{"command": ...}`), but CodeClash's
27+
arena code calls `execute("some shell command")` directly. Normalize so both work.
28+
"""
29+
30+
def execute(self, action: str | dict, cwd: str = "", *, timeout: int | None = None) -> dict:
31+
if isinstance(action, str):
32+
action = {"command": action}
33+
return super().execute(action, cwd, timeout=timeout)
34+
35+
1336
def assert_zero_exit_code(result: dict, *, logger: logging.Logger | None = None) -> dict:
1437
if result.get("returncode", 0) != 0:
1538
msg = f"Command failed with exit code {result.get('returncode')}:\n{result.get('output')}"
@@ -34,10 +57,7 @@ def copy_between_containers(
3457
print(
3558
f"Copy between containers: {src_container.container_id}:{src_path} -> {dest_container.container_id}:{dest_path}"
3659
)
37-
# Some weird stuff happening on AWS where /tmp doesn't work properly
38-
dir = Path.home() / "tmp"
39-
dir.mkdir(parents=True, exist_ok=True)
40-
with tempfile.TemporaryDirectory(dir=dir) as temp_dir:
60+
with tempfile.TemporaryDirectory(dir=_scratch_dir()) as temp_dir:
4161
temp_path = Path(temp_dir) / Path(src_path).name
4262

4363
# Copy from source container to temporary local directory
@@ -151,10 +171,7 @@ def create_file_in_container(
151171
Create a file with given content on a Docker container.
152172
Uses a temporary file on the local filesystem for the transfer.
153173
"""
154-
# Some weird stuff happening on AWS where /tmp doesn't work properly
155-
dir = Path.home() / "tmp"
156-
dir.mkdir(parents=True, exist_ok=True)
157-
with tempfile.NamedTemporaryFile(mode="w", delete=True, suffix=".tmp", dir=dir) as tmp_file:
174+
with tempfile.NamedTemporaryFile(mode="w", delete=True, suffix=".tmp", dir=_scratch_dir()) as tmp_file:
158175
tmp_file.write(content)
159176
tmp_file.flush() # Ensure content is written to disk
160177
tmp_file_path = Path(tmp_file.name)

codeclash/viewer/app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -980,14 +980,14 @@ def get_parent_folder(path):
980980

981981

982982
def format_timestamp(timestamp):
983-
"""Format Unix timestamp as MM/DD HH:MM"""
983+
"""Format Unix timestamp as YY/MM/DD HH:MM"""
984984
if timestamp is None:
985985
return ""
986986
from datetime import datetime
987987

988988
try:
989989
dt = datetime.fromtimestamp(timestamp)
990-
return dt.strftime("%m/%d %H:%M")
990+
return dt.strftime("%y/%m/%d %H:%M")
991991
except (ValueError, OSError):
992992
return ""
993993

codeclash/viewer/static/js/picker.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -840,7 +840,7 @@ function shouldRowBeVisible(row) {
840840
const dateElement = row.querySelector(".date-text");
841841
if (dateElement) {
842842
const dateText = dateElement.textContent.trim();
843-
// Extract just the MM/DD part
843+
// Extract just the date part (before the time)
844844
const rowDate = dateText.split(" ")[0];
845845
if (rowDate !== selectedDate) {
846846
return false;
@@ -1065,7 +1065,7 @@ function handleModelTagClick(event, modelName) {
10651065

10661066
function handleDateClick(event, dateText) {
10671067
event.stopPropagation();
1068-
// Extract just the YYYY-MM-DD part
1068+
// Extract just the date part (before the time)
10691069
const date = dateText.trim().split(" ")[0];
10701070

10711071
// Toggle date filter - if already selected, clear it

codeclash/viewer/static/js/trajectory.js

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,16 @@ function createMessageElement(message, index) {
128128
const messageContent = document.createElement("div");
129129
messageContent.className = "message-content";
130130

131-
// Handle different content types
132-
if (typeof message.content === "string") {
131+
// Handle different content types.
132+
// mini-swe-agent v2 (tool-call) assistant messages keep the command in
133+
// extra.actions / tool_calls instead of in a ```bash block in the text, so they need
134+
// their own renderer. v1 messages have neither and fall through to the text paths below.
135+
const toolActions =
136+
message.extra && Array.isArray(message.extra.actions) ? message.extra.actions : [];
137+
const hasToolCalls = Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
138+
if (toolActions.length > 0 || hasToolCalls) {
139+
messageContent.innerHTML = createToolCallContentHTML(message);
140+
} else if (typeof message.content === "string") {
133141
const lines = message.content.split("\n");
134142
if (lines.length <= 5) {
135143
// Show full content
@@ -260,6 +268,54 @@ function createComplexContentHTML(contentParts) {
260268
return html;
261269
}
262270

271+
function createToolCallContentHTML(message) {
272+
// Render a mini-swe-agent v2 tool-call assistant message: the reasoning text (if any)
273+
// followed by each issued command as a code block (matching the v1 ```bash styling).
274+
275+
// Thought text: content may be a string, an array of content blocks, or null.
276+
let thought = "";
277+
if (typeof message.content === "string") {
278+
thought = message.content;
279+
} else if (Array.isArray(message.content)) {
280+
thought = message.content
281+
.filter((p) => p && p.type === "text" && typeof p.text === "string")
282+
.map((p) => p.text)
283+
.join("\n");
284+
}
285+
286+
// Commands: prefer the parsed actions, fall back to the raw tool_calls.
287+
let commands = [];
288+
if (message.extra && Array.isArray(message.extra.actions)) {
289+
commands = message.extra.actions.map((a) => a.command).filter(Boolean);
290+
}
291+
if (!commands.length && Array.isArray(message.tool_calls)) {
292+
commands = message.tool_calls
293+
.map((tc) => {
294+
try {
295+
return JSON.parse(tc.function.arguments).command;
296+
} catch (e) {
297+
return tc.function && tc.function.arguments;
298+
}
299+
})
300+
.filter(Boolean);
301+
}
302+
303+
let html = '<div class="message-content-full">';
304+
if (thought.trim()) {
305+
html += `<div class="message-text"><pre>${escapeHtml(thought)}</pre></div>`;
306+
}
307+
commands.forEach((cmd) => {
308+
html += `<div class="code-block"><pre><code>${escapeHtml(cmd)}</code></pre></div>`;
309+
});
310+
if (!thought.trim() && !commands.length) {
311+
html += `<div class="message-text"><pre>${escapeHtml(
312+
JSON.stringify(message.content, null, 2),
313+
)}</pre></div>`;
314+
}
315+
html += "</div>";
316+
return html;
317+
}
318+
263319
function escapeHtml(text) {
264320
const div = document.createElement("div");
265321
div.textContent = text;

codeclash/viewer/templates/includes/message.html

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,25 @@
55
{{ message_role_upper }} #{{ loop.index }}
66
</span>
77
<div class="message-content">
8-
{% if message.content is string %}
8+
{% set tool_actions = message.extra.actions if (message.extra and message.extra.actions) else [] %}
9+
{% if tool_actions %}
10+
{# mini-swe-agent v2 tool-call assistant message: reasoning text + issued command(s).
11+
The command lives in extra.actions rather than a ```bash block in the text. #}
12+
<div class="message-content-full">
13+
{% if message.content is string and message.content|trim %}
14+
<div class="message-text">
15+
<pre>{{ message.content }}</pre>
16+
</div>
17+
{% endif %}
18+
{% for action in tool_actions %}
19+
{% if action.command %}
20+
<div class="code-block">
21+
<pre><code>{{ action.command }}</code></pre>
22+
</div>
23+
{% endif %}
24+
{% endfor %}
25+
</div>
26+
{% elif message.content is string %}
927
{# Simple string content #}
1028
{% set line_count = message.content.count('\n') + 1 %}
1129
{% if line_count <= 5 %}

0 commit comments

Comments
 (0)