rein-aharness/agent_harness/adapter.py
tegwick 4144eba160 feat: instance manifest, tool profiles, metrics, and budget enforcement
Land HARNESS-WP-0001 T01/T02/T04/T05: extend ADR-005 schedule.yml with
harness fields, named tool-profile registry, ADR-004 metrics writes, and
BudgetTracker wiring. CLI gains validate/profiles; task-file path kept.
2026-07-17 23:49:03 +02:00

97 lines
3.2 KiB
Python

"""Agentic Claude Code adapter.
llm-connect's ClaudeCodeAdapter is a text-generation adapter (`claude
--print`, no working directory, no tool grants). An executor run needs an
*agentic* session: file edits and git commits inside the target repo, under
a hard tool allow-list. This adapter subclasses it, keeping the llm-connect
LLMAdapter interface so a hosted adapter can be swapped in later, and adds:
- cwd pinned to the target repo
- --permission-mode acceptEdits
- allow-list from a named tool profile (default: green-commit-only)
"""
from __future__ import annotations
import subprocess
from pathlib import Path
from llm_connect.claude_code import ClaudeCodeAdapter
from llm_connect.exceptions import LLMSubprocessError, LLMTimeoutError
from llm_connect.models import LLMResponse, RunConfig
from agent_harness.profiles import ToolProfile, get_profile
# Backward-compatible alias for the seed profile allow-list string.
ALLOWED_TOOLS = get_profile("green-commit-only").allowed_tools
class AgenticClaudeCodeAdapter(ClaudeCodeAdapter):
def __init__(
self,
workdir: Path,
*,
tool_profile: str | ToolProfile = "green-commit-only",
**kwargs,
):
super().__init__(**kwargs)
self._workdir = workdir
if isinstance(tool_profile, ToolProfile):
self._profile = tool_profile
else:
self._profile = get_profile(tool_profile)
@property
def tool_profile(self) -> ToolProfile:
return self._profile
def _build_command(self, config: RunConfig) -> list[str]:
cmd = [
self._cli_path,
"--print",
"--permission-mode",
"acceptEdits",
"--allowedTools",
self._profile.allowed_tools,
]
if self._model:
cmd.extend(["--model", self._model])
return cmd
def execute_prompt(self, prompt: str, config: RunConfig) -> LLMResponse:
self._preflight_budget(config)
cmd = self._build_command(config)
timeout = config.timeout_seconds or self._config.timeout_seconds
try:
result = subprocess.run(
cmd,
input=prompt,
capture_output=True,
text=True,
timeout=timeout,
cwd=self._workdir,
)
except subprocess.TimeoutExpired as exc:
raise LLMTimeoutError(
f"claude CLI timed out after {timeout}s", cause=exc
) from exc
if result.returncode != 0:
raise LLMSubprocessError(
f"claude CLI exited with code {result.returncode}",
return_code=result.returncode,
stderr=result.stderr,
)
response = LLMResponse(
content=result.stdout,
model=self._model or "claude-code-cli",
usage={},
finish_reason="stop",
metadata={
"provider": "claude-code-agentic",
"cli_path": self._cli_path,
"workdir": str(self._workdir),
"tool_profile": self._profile.name,
},
)
self._consume_budget(config, response)
return response