Files
magnus919_agent-skills/eval_runner/cli_adapter.py
T
usernameandfactory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> 476d7e11b0 feat(ci): add linting, formatting, coverage, and security configs
Add ruff linter/formatter with pre-commit hooks, pytest-cov with 60%
coverage threshold, CODEOWNERS, Dependabot for pip/GHA updates, and
.env.example. Auto-fix existing ruff violations across eval_runner/
and scripts/.

10 agent-readiness criteria resolved: lint_config, formatter,
pre_commit_hooks, naming_consistency, dead_code_detection,
test_coverage_thresholds, test_performance_tracking, codeowners,
dependency_update_automation, env_template.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-07-29 17:43:41 -04:00

127 lines
4.3 KiB
Python

"""Subprocess-based adapter for non-interactive CLI agent harnesses.
Executes a configurable command with the case prompt piped to stdin or passed
as an argument. Captures stdout as the response, stderr for diagnostics, and
records exit code, timing, and any artifacts produced in the output directory.
This is the first real harness adapter. It supports any agent CLI that can:
- accept a prompt non-interactively (via stdin or --prompt flag)
- write its response to stdout
- operate within a working directory
Configuration via harness_config:
command: list[str] — the command to run (e.g. ["opencode", "--print"])
prompt_mode: "stdin" | "arg" — how to pass the prompt (default: "stdin")
prompt_flag: str — flag name if prompt_mode is "arg" (default: "--prompt")
timeout_seconds: int — max wall time (default: 120)
extra_args: list[str] — additional CLI arguments
"""
from __future__ import annotations
import os
import subprocess
import time
from .models import AdapterInput, AdapterOutput, ExitStatus, ToolEvent
class CliSubprocessAdapter:
"""Runs an agent CLI as a subprocess and normalizes the result."""
def __init__(
self,
command: list[str],
*,
prompt_mode: str = "stdin",
prompt_flag: str = "--prompt",
timeout_seconds: int = 120,
extra_args: list[str] | None = None,
):
self._command = command
self._prompt_mode = prompt_mode
self._prompt_flag = prompt_flag
self._timeout_seconds = timeout_seconds
self._extra_args = extra_args or []
@property
def name(self) -> str:
return "cli-subprocess"
@property
def version(self) -> str:
return "0.1.0"
def execute(self, input: AdapterInput) -> AdapterOutput:
timeout = input.limits.get("timeout_seconds", self._timeout_seconds)
cmd = list(self._command) + list(self._extra_args)
stdin_data: str | None = None
if self._prompt_mode == "stdin":
stdin_data = input.case.prompt
elif self._prompt_mode == "arg":
cmd += [self._prompt_flag, input.case.prompt]
env = dict(os.environ)
env.update(input.env)
input.work_dir.mkdir(parents=True, exist_ok=True)
input.output_dir.mkdir(parents=True, exist_ok=True)
start = time.monotonic()
try:
proc = subprocess.run(
cmd,
input=stdin_data,
capture_output=True,
text=True,
timeout=timeout,
cwd=str(input.work_dir),
env=env,
)
elapsed_ms = (time.monotonic() - start) * 1000
exit_status = ExitStatus.COMPLETED if proc.returncode == 0 else ExitStatus.ERROR
artifacts = [f.name for f in input.output_dir.iterdir() if f.is_file()]
tool_events = []
if proc.stderr:
tool_events.append(
ToolEvent(
name="subprocess_stderr",
arguments={"command": " ".join(cmd)},
result_summary=proc.stderr[:500],
)
)
return AdapterOutput(
exit_status=exit_status,
response=proc.stdout or None,
activation_evidence=None,
artifacts=artifacts,
environment_state={"returncode": proc.returncode},
tool_events=tool_events,
duration_ms=elapsed_ms,
token_usage=None,
raw_trace_path=None,
error=proc.stderr[:1000] if proc.returncode != 0 else None,
)
except subprocess.TimeoutExpired:
elapsed_ms = (time.monotonic() - start) * 1000
return AdapterOutput(
exit_status=ExitStatus.TIMEOUT,
response=None,
error=f"process exceeded {timeout}s timeout",
duration_ms=elapsed_ms,
)
except FileNotFoundError as exc:
elapsed_ms = (time.monotonic() - start) * 1000
return AdapterOutput(
exit_status=ExitStatus.ERROR,
response=None,
error=f"command not found: {exc}",
duration_ms=elapsed_ms,
)