Skip to main content

Adding a Harness

A harness in AlphaDiana is an Agent: a class that turns a benchmark task into an answer (and a trajectory of how it got there). The baseline direct_llm is a single chat call; the agentic harnesses opencode, openclaw, and zeroclaw drive an external CLI inside a sandbox. They all implement the same small interface, so adding a new one is a matter of subclassing Agent, returning an AgentResponse, and registering the class.

1. Subclass Agent

The base class lives at alphadiana/harness/base.py. It is an ABC with two abstract methods and one optional hook:

from typing import Any, Optional

from alphadiana.benchmarks.base import BenchmarkTask
from alphadiana.harness.base import Agent, AgentResponse


class MyAgent(Agent):
name = "myagent" # registry key, used as agent.name in config
version = "" # set by the runner from config.agent_version

def setup(self, config: dict) -> None:
# Read agent.config here; store endpoint, model, timeouts, etc.
...

def solve(self, task: BenchmarkTask, sandbox: Optional[Any] = None) -> AgentResponse:
# Do the work; return an AgentResponse.
...

def teardown(self) -> None:
# Optional; default is a no-op. Clean up containers/processes here.
...

Set the two class attributes name: str and version: str. The name is the registry key that a config selects via agent.name. The runner instantiates the class, assigns agent.version from the config, then calls setup() once and solve() per task in alphadiana/engine/runner.py.

setup(config)

config is the agent.config block from the YAML. Read every key you need here and stash it on self. By convention, treat the literal API key EMPTY (case-insensitive) as unset and fall back to the environment, and use sk-EMPTY in configs for keyless local vLLM (the validator rejects literal EMPTY). The shared logprob helper resolve_logprob_capture_config(config) (alphadiana/harness/proxies/logprob_capture.py) gives you a consistent {enabled, top_logprobs} if you want observability parity with the other harnesses.

solve(task, sandbox)

task is a BenchmarkTask (task.problem, task.ground_truth, task.metadata, etc.). sandbox is provided when the config sets a sandbox.name; the baseline path leaves it None. Do the actual solving and return an AgentResponse.

2. Return an AgentResponse

AgentResponse is a @dataclass in alphadiana/harness/base.py. General answer scorers grade answer, but benchmark-specific scorers may also consume response metadata or artifacts. Check every target scorer before defining the contract.

FieldTypePurpose
answerAnyExtracted answer used by general answer scorers. None marks a failed/timed-out attempt.
trajectorylist[dict]Per-step record of the run.
raw_outputstrFull model/CLI output.
token_usagedictPrompt/completion token counts.
token_entropy_statsdictLogprob-derived entropy (from the logprob helper).
wall_time_secfloatEnd-to-end latency.
metadatadictFree-form. Failure reasons, transport, status flags.

The dataclass also carries extended observability fields absorbed from codex-dev (all optional, default-empty): reasoning_trajectory, request_messages, response_json, sandbox_id, gateway_url, artifact_manifest, gateway_log_excerpt, workspace_snapshot_paths, workspace_file_contents, sandbox_metadata, system_prompt, and finish_reason. Populate whatever your harness can produce; they make a run auditable, and scorer-specific fields can affect scoring. Benchmark-specific adapters may consume artifact and runtime metadata. Set answer = None and record a reason in metadata (for example failure_reason) when a run cannot produce a gradable answer.

3. Register the class

The registry is AgentRegistry in alphadiana/harness/registry.py, a classmethod-only singleton over a class-level _registry: dict[str, Type[Agent]].

MethodBehavior
AgentRegistry.register(name, cls)Add cls under string key name.
AgentRegistry.get(name)Return the class; raises KeyError listing available names if missing.
AgentRegistry.list()Sorted list of registered names.

Register at the bottom of your agent module so the call fires on import. A register_agent(name) decorator exists, but the shipped agents register directly in their implementation modules:

# Example module: alphadiana/harness/<your_agent>/agent.py
AgentRegistry.register("myagent", MyAgent)

4. Wire the import into the Runner

Registration is import-triggered, not auto-discovered. The module-level register() call only runs if the module is imported, and the engine imports each agent module explicitly in Runner.setup() in alphadiana/engine/runner.py. Add your module to that list:

# alphadiana/engine/runner.py, in Runner.setup()
import alphadiana.harness.direct_llm # noqa: F401
import alphadiana.harness.openclaw.agent # noqa: F401
import alphadiana.harness.opencode.agent # noqa: F401
import alphadiana.harness.zeroclaw.agent # noqa: F401
import alphadiana.harness.myagent.agent # noqa: F401 # <- add this

The same import-for-side-effect pattern registers benchmarks, sandboxes, and scorers. Forgetting the import line is the most common mistake — the agent looks registered (the register() call is in the file) but AgentRegistry.get never sees it, so agent.name: myagent fails with a KeyError listing the names that were imported.

5. Select it from a config

Once registered and imported, a config selects the agent by its name:

agent:
name: myagent
config:
model: Qwen/Qwen3.5-27B
api_base: http://127.0.0.1:8011/v1
api_key: sk-EMPTY

Validate, then run:

python -m alphadiana.cli validate <config.yaml>
python -m alphadiana.cli run <config.yaml>

Optional: skills

If your harness should expose AlphaDiana skill bundles to the model, read the agent.config.skill_folder key in setup() and mount the bundle into the sandbox. The existing agents resolve it with a per-harness _resolve_skill_folder: empty disables it; an absolute path is used as-is; a value with / resolves against the cwd; a bare name resolves to alphadiana/harness/skills/<name>/ (for example advanced-maths). Skills are not auto-injected into context, so your system prompt must instruct the model to read the mounted SKILL.md.