Skip to content
Open
27 changes: 27 additions & 0 deletions devops_bench/agents/cli/claude_code/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copyright 2026 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Claude Code CLI agent harness, named to distinguish it from the Claude model.

The harness driver lives in :mod:`.agent` and the stream-json parser in
:mod:`.parsing`. Importing this package self-registers the agent under the
``"claude"`` key via ``@AGENTS.register``.
"""

from __future__ import annotations

from devops_bench.agents.cli.claude_code.agent import ClaudeCodeAgent
from devops_bench.agents.cli.claude_code.parsing import parse_stream_json

__all__ = ["ClaudeCodeAgent", "parse_stream_json"]
398 changes: 398 additions & 0 deletions devops_bench/agents/cli/claude_code/agent.py

Large diffs are not rendered by default.

378 changes: 378 additions & 0 deletions devops_bench/agents/cli/claude_code/parsing.py

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions devops_bench/agents/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,9 @@ class AgentConfig:
only for tests / local debug) — :meth:`from_env` always falls back
to the 600s default since ``AGENT_TIMEOUT_SEC`` has no sentinel for
"disabled".
max_turns: Safety cap on the API agent's tool-use loop turns; flows from
``AGENT_MAX_TURNS``. ``None`` uses the agent's built-in default.
max_turns: Safety cap on the API agent's tool-use loop turns and on the
Claude CLI's ``--max-turns``; flows from ``AGENT_MAX_TURNS``.
``None`` uses the agent's built-in default.
capabilities: Aggregate of the MCP / skills / rules bindings granted
for the run. Constructed by the orchestrator from a benchmark
catalog (no GKE-specific strings live in agent code).
Expand Down
13 changes: 10 additions & 3 deletions devops_bench/agents/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,14 @@ def errored(cls, msg: str, *, latency: float = 0.0) -> AgentResult:
latency: Elapsed seconds before the failure, when available.

Returns:
An :class:`AgentResult` with empty trajectory and the message in
both ``output`` and ``errors``.
An :class:`AgentResult` with empty trajectory, the canonical
all-``None`` token shape, and the message in both ``output`` and
``errors``.
"""
return cls(output=f"Error: {msg}", trajectory=[], latency=latency, errors=[msg])
return cls(
output=f"Error: {msg}",
trajectory=[],
tokens=empty_tokens(),
latency=latency,
errors=[msg],
)
25 changes: 21 additions & 4 deletions devops_bench/evalharness/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
# registry, with no edit here.
_BUILTIN_AGENT_MODULES: tuple[str, ...] = (
"devops_bench.agents.cli.gemini_cli",
"devops_bench.agents.cli.claude_code",
"devops_bench.agents.cli.openclaw",
"devops_bench.agents.cli.antigravity",
"devops_bench.agents.api.agent",
Expand All @@ -79,6 +80,7 @@
# Aliases normalized to canonical agent keys before registry lookup.
_AGENT_TYPE_ALIASES: dict[str, str] = {
"gemini-cli": "gemini",
"claude-code": "claude",
}

# Default agent type when neither --agent-type nor BENCH_AGENT_TYPE is set.
Expand Down Expand Up @@ -123,6 +125,17 @@ def _ensure_builtin_agents_registered() -> None:
_log.debug("optional agent module %s not importable: %s", module, exc)


def _canonical_agent_type(agent_type: str) -> str:
"""Normalize an agent-type alias to its canonical registry key.

The single source of truth for both registry lookup and result recording,
so an arm selected via a friendly alias (``claude-code`` / ``gemini-cli``)
aggregates under the same ``harness`` / ``setup_id`` as the canonical key
instead of splitting into a second dashboard setup.
"""
return _AGENT_TYPE_ALIASES.get(agent_type, agent_type)


class DefaultEvalHarness(Harness):
"""Standard harness wiring every component into one pipeline.

Expand Down Expand Up @@ -235,7 +248,7 @@ def resolve_agent(self, agent_type: str) -> Any:
canonical key.
"""
_ensure_builtin_agents_registered()
key = _AGENT_TYPE_ALIASES.get(agent_type, agent_type)
key = _canonical_agent_type(agent_type)
agent_cls = AGENTS.get(key)
if agent_cls is None:
raise NotRegisteredError(AGENTS.name, key, AGENTS.keys())
Expand Down Expand Up @@ -674,14 +687,18 @@ def _write_run_artifacts(self, run_dir: Path, detailed_results: list[dict[str, A
augmentation = derive_augmentation(
{"use_mcp": self.use_mcp, "skills": list(self._granted_skill_paths)}
)
model = self._agent_config.model or self._agent_config.provider or self.agent_type
# Record the canonical harness key so an arm selected via a friendly
# alias (e.g. ``claude-code`` / ``gemini-cli``) aggregates with the
# canonical key rather than splitting into a second dashboard setup.
harness = _canonical_agent_type(self.agent_type)
model = self._agent_config.model or self._agent_config.provider or harness
manifest = Manifest(
schema_version=SCHEMA_VERSION,
run_id=run_dir.name,
t=datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%dT%H:%M:%SZ"),
setup_id=results_setup_id(model, self.agent_type, augmentation),
setup_id=results_setup_id(model, harness, augmentation),
model=model,
harness=self.agent_type,
harness=harness,
augmentation=augmentation,
)
rows = build_rows(detailed_results, manifest)
Expand Down
6 changes: 3 additions & 3 deletions docs/components/model_providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ API-key variable, or keyless auth — those belong to the provider contract in

## Supported providers and models

Every harness — the `api` runner and the `gemini`/`openclaw` CLIs — resolves
`AGENT_PROVIDER` through one shared contract
Every harness — the `api` runner and the `gemini`/`claude`/`openclaw` CLIs —
resolves `AGENT_PROVIDER` through one shared contract
(`devops_bench/core/model_providers.py`). A provider resolves to an *adapter
family* (which `LLMClient` the `api` harness builds), a *backend* hint
(genai/vertex/bedrock), the *openclaw wire-provider*, and the *API-key env
Expand Down Expand Up @@ -96,7 +96,7 @@ final fallback.

> [!NOTE]
> **All harnesses share one provider contract.** The `api` runner and the
> `gemini`/`openclaw` CLIs all resolve `AGENT_PROVIDER` through
> `gemini`/`claude`/`openclaw` CLIs all resolve `AGENT_PROVIDER` through
> `devops_bench/core/model_providers.py`. The `api` harness uses it to pick the
> adapter family and backend for `get_model()`; the CLI harnesses use it to route
> `AGENT_API_KEY` onto the binary's provider-specific env var(s) (e.g. `google` →
Expand Down
7 changes: 4 additions & 3 deletions docs/how-to/add-a-model-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,10 @@ absent.

`AGENT_PROVIDER` resolves through the shared contract in
`devops_bench/core/model_providers.py` — the one place every harness (the `api`
runner and the `gemini`/`openclaw` CLIs) reads. If your provider needs aliases, a
distinct backend, a non-default API-key env var, or keyless (ADC-style) auth, add
a `ProviderSpec` row to `_SPECS` and its alias(es) to `_ALIASES` there:
runner and the `gemini`/`claude`/`openclaw` CLIs) reads. If your provider needs
aliases, a distinct backend, a non-default API-key env var, or keyless
(ADC-style) auth, add a `ProviderSpec` row to `_SPECS` and its alias(es) to
`_ALIASES` there:

```python
_SPECS["your-key"] = ProviderSpec(
Expand Down
Loading