Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions codeclash/agents/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from codeclash.agents.abstract import Player
from codeclash.agents.dummy import Dummy
from codeclash.agents.minisweagent import MiniSWEAgent
from codeclash.agents.utils import GameContext
from codeclash.constants import DIR_WORK
from codeclash.games.abstract import CodeGame


def get_agent(config: dict, game: CodeGame) -> Player:
def get_agent(config: dict, prompts: dict, game: CodeGame) -> Player:
agents = {
"dummy": Dummy,
"mini": MiniSWEAgent,
Expand All @@ -14,12 +16,16 @@ def get_agent(config: dict, game: CodeGame) -> Player:
environment = game.get_environment(
f"{game.game_id}.{config['name']}"
) # NOTE: MUST be branch_name (defined in agents/abstract.py)
template_vars = {
"game_name": game.name,
"game_id": game.game_id,
"rounds": game.rounds,
"round": 1,
"player_id": config["name"],
"game_description": game.config.get("description", ""),
}
return agents(config, environment, template_vars)
return agents(
config,
environment,
GameContext(
id=game.game_id,
name=game.name,
player_id=config["name"],
prompts=prompts,
round=1,
rounds=game.rounds,
working_dir=str(DIR_WORK),
),
)
24 changes: 12 additions & 12 deletions codeclash/agents/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from dotenv import load_dotenv
from minisweagent import Environment

from codeclash.agents.utils import GameContext
from codeclash.constants import DIR_LOGS, GH_ORG
from codeclash.utils.environment import assert_zero_exit_code
from codeclash.utils.log import get_logger
Expand All @@ -16,39 +17,38 @@ def __init__(
self,
config: dict,
environment: Environment,
template_vars: dict,
game_context: GameContext,
):
self.config = config
self.name = config["name"]
self.environment = environment
self.template_vars = template_vars
self.game_context = game_context
self.game_context.render_and_set_prompts()
self.logger = get_logger(
self.name,
log_path=DIR_LOGS / template_vars["game_id"] / f"{self.name}.log",
log_path=DIR_LOGS / game_context.id / f"{self.name}.log",
emoji="👤",
)

@property
def branch_name(self):
"""Get the branch name for the agent's codebase."""
return f"{self.template_vars['game_id']}.{self.name}"
return f"{self.game_context.id}.{self.name}"

def commit(self):
"""Commit changes to the agent's codebase."""
rounds = self.template_vars["rounds"]
r, rounds = self.game_context.round, self.game_context.rounds
for cmd in [
"git add -A",
f"git commit --allow-empty -m 'Round {self.round}/{rounds} Update'",
f"git commit --allow-empty -m 'Round {r}/{rounds} Update'",
]:
assert_zero_exit_code(self.environment.execute(cmd), logger=self.logger)
self.logger.info(
f"Committed changes for {self.name} for round {self.round}/{rounds}"
)
self.logger.info(f"Committed changes for {self.name} for round {r}/{rounds}")

def on_round_update(self, new_round: int):
"""Update the agent's round to match the game round."""
self.round = new_round
self.template_vars["round"] = new_round
self.game_context.round = new_round
self.game_context.render_and_set_prompts()

def push(self):
"""Push codebase to a branch on the game's remote repository."""
Expand All @@ -58,7 +58,7 @@ def push(self):

for cmd in [
"git remote remove origin",
f"git remote add origin https://x-access-token:{token}@github.com/{GH_ORG}/{self.template_vars['game_name']}.git",
f"git remote add origin https://x-access-token:{token}@github.com/{GH_ORG}/{self.game_context.name}.git",
f"git push origin {self.branch_name}",
]:
assert_zero_exit_code(self.environment.execute(cmd), logger=self.logger)
Expand Down
26 changes: 17 additions & 9 deletions codeclash/agents/minisweagent.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
from rich.console import Console

from codeclash.agents.abstract import Player
from codeclash.agents.utils import resolve_api_key
from codeclash.agents.utils import GameContext, resolve_api_key
from codeclash.constants import DIR_LOGS
from codeclash.utils.environment import copy_file_to_container


class ClashAgent(DefaultAgent):
Expand All @@ -30,15 +31,15 @@ def __init__(
model: Model,
env: Environment,
name: str,
template_vars: dict,
game_context: GameContext,
*,
logger: logging.Logger,
config_class: Callable = AgentConfig,
**kwargs,
):
super().__init__(model, env, config_class=config_class, **kwargs)
self.name = name
self.template_vars = template_vars
self.game_context = game_context
self.console = Console()
self.logger = logger

Expand All @@ -56,7 +57,7 @@ def render_template(self, template: str, **kwargs) -> str:
| asdict(self.env.config)
| asdict(self.model.config)
| platform.uname()._asdict()
| self.template_vars
| self.game_context.to_dict()
)
return Template(template).render(**kwargs, **cs, **os.environ)

Expand All @@ -68,16 +69,18 @@ def run(self) -> tuple[str, str]:
class MiniSWEAgent(Player):
"""Player with agentic code editing capabilities"""

def __init__(self, config: dict, environment: Environment, template_vars: dict):
super().__init__(config, environment=environment, template_vars=template_vars)
def __init__(
self, config: dict, environment: Environment, game_context: GameContext
):
super().__init__(config, environment=environment, game_context=game_context)
self.agent = ClashAgent(
LitellmModel(
model_name=config["model"],
model_kwargs={"api_key": resolve_api_key(config["model"])},
),
self.environment,
self.name,
template_vars,
game_context,
logger=self.logger,
**yaml.safe_load(Path(config["config"]).read_text())["agent"],
)
Expand All @@ -93,11 +96,16 @@ def run(self):
result = exc_message
print(exc_message)
finally:
traj_path = (
DIR_LOGS
/ self.game_context.id
/ f"{self.name}_r{self.game_context.round}.traj.json"
)
save_traj(
self.agent, # type: ignore
DIR_LOGS
/ f"{self.template_vars['game_id']}/{self.name}_r{self.template_vars['round']}.traj.json",
traj_path,
exit_status=exit_status,
result=result,
)
copy_file_to_container(self.environment, traj_path, traj_path)
self.commit()
44 changes: 44 additions & 0 deletions codeclash/agents/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import os
from dataclasses import asdict, dataclass

from dotenv import load_dotenv
from jinja2 import Template

load_dotenv()

Expand All @@ -10,3 +12,45 @@ def resolve_api_key(model: str) -> str:
return os.getenv("ANTHROPIC_API_KEY")
if "gpt" in model:
return os.getenv("OPENAI_API_KEY")


@dataclass
class GameContext:
"""
A class that gives agent access to a partial view of the game state.

NOTE: Instead of passing `game` directly as a reference to the agent,
we create this interface instead to make the communication of game state
more explicit and controlled. We go with this loose coupling to avoid
making the agent too dependent on the entire game object.
"""

id: str
name: str
player_id: str
prompts: dict
round: int
rounds: int
working_dir: str

def render_and_set_prompts(self):

@klieret klieret Aug 14, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see, this is because we need to render variables within the game description?

But wouldn't the clean way then simply be to have a render_prompts method that gets called when we use this object in the agent?

Why do we assign it with setattr and then later delete attrs in to_dict etc.? That feels pretty unnatural and confusing. Normally, we just want to track state in the object (without any duplication), and then either use properties or methods to transform it (but maybe I'm missing something)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah ok yeah this could be cleaner. But basically

  • we define a set of prompts in CodeClash configs
  • and then we define a set of prompts in Mini configs

The Mini config will reference ({{...}}) a prompt defined in the CodeClash config. But then the CodeClash config will reference the game state, and it needs to be updated as the game proceeds (e.g. round).

So then if i do Template(Mini template).render(CodeClash template), it won't populate the CodeClash template correctly with the metadata.

So this extra function is essentially to populate the CodeClash template first. Basically, this was the approach I came up to handle render a template, that is then rendered in another template.

There could be a more elegant way to do this. I was going for the simplest, most flexible solution. I think it's ok, but open to changing this if there's something obviously easier?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally happy to just merge this and if I can come up with something more elegant, I can just create a PR or something? I was thinking about this yesterday a little bit and I think there might be slightly more elegant ways to do this, but I need to experiment a bit (it would be equivalent)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sg sg yeah like we discussed in meeting - I'll resolve ur comments right now 🙏🏼 and then just merge, so we can just move on for now.

"""Render and set prompts using the current game context."""
context = asdict(self)
del context["prompts"]
for key, template_str in self.prompts.items():
rendered = Template(template_str).render(**context)
setattr(self, key, rendered)

def to_dict(self):
"""Convert the GameContext to a dictionary, including dynamically added attributes."""
result = asdict(self)
declared = set(self.__dataclass_fields__)
for attr in dir(self):
if (
not attr.startswith("_")
and attr not in declared
and not callable(getattr(self, attr))
):
result[attr] = getattr(self, attr)
del result["prompts"]
return result
7 changes: 5 additions & 2 deletions codeclash/games/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@

from codeclash.agents.abstract import Player
from codeclash.constants import DIR_LOGS, DIR_WORK, GH_ORG
from codeclash.games.utils import copy_between_containers, copy_file_from_container
from codeclash.utils.environment import assert_zero_exit_code
from codeclash.utils.environment import (
assert_zero_exit_code,
copy_between_containers,
copy_file_from_container,
)
from codeclash.utils.log import get_logger


Expand Down
2 changes: 1 addition & 1 deletion codeclash/games/robocode/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from codeclash.agents.abstract import Player
from codeclash.games.abstract import CodeGame
from codeclash.games.utils import copy_file_to_container
from codeclash.utils.environment import copy_file_to_container


class RoboCodeGame(CodeGame):
Expand Down
101 changes: 0 additions & 101 deletions codeclash/games/utils.py

This file was deleted.

Loading