From 49055671b3d3cf03c292a5591c248c10b81c63dd Mon Sep 17 00:00:00 2001 From: Protocol Zero <257158451+Protocol-zero-0@users.noreply.github.com> Date: Tue, 26 May 2026 08:54:48 +0000 Subject: [PATCH] =?UTF-8?q?feat(roles):=20add=20claude=5Fcli=20provider=20?= =?UTF-8?q?=E2=80=94=20run=20kernel=20via=20Claude=20Code=20CLI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a third LLM provider alongside anthropic and openai. Set `llm.provider: claude_cli` in evolution.yml to drive planner / evaluator / goal_evaluator / strategist through the local `claude` binary instead of an API key. Why: the bundled examples (oss_fix_demo) already use claude CLI for the executor, but the planning roles still required an Anthropic API key. With this, a Claude Pro subscription is enough to run the full kernel — lowers the bar for evaluators / readers who try the README quickstart. Implementation: - Each role gets a `_call_claude_cli(prompt, model)` that shells out to `claude -p --model --output-format json`, reads `result` from the JSON envelope. Planner / evaluator also read `total_cost_usd` and total tokens so the kernel-level cost guards keep working. - All four roles route through the existing `provider` switch; default behavior unchanged (provider defaults stay anthropic / openai). - No new dependency — uses stdlib subprocess + json. Defaults: opt-in only. Existing configs continue to work bit-for-bit. Targets a v1.2.0 minor release. --- roles/evaluator.py | 17 +++++++++++++++++ roles/goal_evaluator.py | 17 +++++++++++++++++ roles/planner.py | 25 +++++++++++++++++++++++++ roles/strategist.py | 17 +++++++++++++++++ 4 files changed, 76 insertions(+) diff --git a/roles/evaluator.py b/roles/evaluator.py index f575f7c..340a936 100755 --- a/roles/evaluator.py +++ b/roles/evaluator.py @@ -41,6 +41,23 @@ def _call_llm(prompt: str, cfg: dict) -> tuple[str, int, float]: tokens = resp.usage.total_tokens return resp.choices[0].message.content, tokens, tokens * 3e-6 + if provider == "claude_cli": + import subprocess + proc = subprocess.run( + ["claude", "-p", "--model", model, "--output-format", "json"], + input=prompt, + capture_output=True, + text=True, + ) + if proc.returncode != 0: + raise RuntimeError(f"claude -p failed (exit {proc.returncode}): {proc.stderr[:200]}") + data = json.loads(proc.stdout) + text = data.get("result", "") + usage = data.get("usage", {}) + tokens = usage.get("input_tokens", 0) + usage.get("output_tokens", 0) + cost = float(data.get("total_cost_usd", 0.0)) + return text, tokens, cost + raise ValueError(f"unknown llm.provider: {provider!r}") diff --git a/roles/goal_evaluator.py b/roles/goal_evaluator.py index 3085a1c..e816a85 100644 --- a/roles/goal_evaluator.py +++ b/roles/goal_evaluator.py @@ -38,6 +38,21 @@ def _call_openai(prompt: str, model: str, api_key_env: str) -> str: return resp.choices[0].message.content +def _call_claude_cli(prompt: str, model: str) -> str: + """Call Claude via the local Claude Code CLI (no API key required).""" + import subprocess + proc = subprocess.run( + ["claude", "-p", "--model", model, "--output-format", "json"], + input=prompt, + capture_output=True, + text=True, + ) + if proc.returncode != 0: + raise RuntimeError(f"claude -p failed (exit {proc.returncode}): {proc.stderr[:200]}") + data = json.loads(proc.stdout) + return data.get("result", "") + + def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--input", required=True) @@ -81,6 +96,8 @@ def main() -> None: text = _call_anthropic(prompt, model, api_key_env) elif provider == "openai": text = _call_openai(prompt, model, api_key_env) + elif provider == "claude_cli": + text = _call_claude_cli(prompt, model) else: print(f"error: unknown llm.provider: {provider!r}", file=sys.stderr) sys.exit(1) diff --git a/roles/planner.py b/roles/planner.py index ec2f14f..06e2308 100755 --- a/roles/planner.py +++ b/roles/planner.py @@ -45,6 +45,29 @@ def _call_openai(prompt: str, model: str, api_key_env: str) -> tuple[str, int, f return resp.choices[0].message.content, tokens, cost +def _call_claude_cli(prompt: str, model: str) -> tuple[str, int, float]: + """Call Claude via the local Claude Code CLI (no API key required). + + Uses --output-format json to recover the response text plus real + token / cost numbers, so kernel-level cost guards keep working. + """ + import subprocess + proc = subprocess.run( + ["claude", "-p", "--model", model, "--output-format", "json"], + input=prompt, + capture_output=True, + text=True, + ) + if proc.returncode != 0: + raise RuntimeError(f"claude -p failed (exit {proc.returncode}): {proc.stderr[:200]}") + data = json.loads(proc.stdout) + text = data.get("result", "") + usage = data.get("usage", {}) + tokens = usage.get("input_tokens", 0) + usage.get("output_tokens", 0) + cost = float(data.get("total_cost_usd", 0.0)) + return text, tokens, cost + + def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--input", required=True) @@ -104,6 +127,8 @@ def main() -> None: text, tokens, cost = _call_anthropic(prompt, model, api_key_env) elif provider == "openai": text, tokens, cost = _call_openai(prompt, model, api_key_env) + elif provider == "claude_cli": + text, tokens, cost = _call_claude_cli(prompt, model) else: print(f"error: unknown llm.provider: {provider!r}", file=sys.stderr) sys.exit(1) diff --git a/roles/strategist.py b/roles/strategist.py index 74d6af9..02db832 100644 --- a/roles/strategist.py +++ b/roles/strategist.py @@ -38,6 +38,21 @@ def _call_openai(prompt: str, model: str, api_key_env: str) -> str: return resp.choices[0].message.content +def _call_claude_cli(prompt: str, model: str) -> str: + """Call Claude via the local Claude Code CLI (no API key required).""" + import subprocess + proc = subprocess.run( + ["claude", "-p", "--model", model, "--output-format", "json"], + input=prompt, + capture_output=True, + text=True, + ) + if proc.returncode != 0: + raise RuntimeError(f"claude -p failed (exit {proc.returncode}): {proc.stderr[:200]}") + data = json.loads(proc.stdout) + return data.get("result", "") + + def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--input", required=True) @@ -80,6 +95,8 @@ def main() -> None: text = _call_anthropic(prompt, model, api_key_env) elif provider == "openai": text = _call_openai(prompt, model, api_key_env) + elif provider == "claude_cli": + text = _call_claude_cli(prompt, model) else: print(f"error: unknown llm.provider: {provider!r}", file=sys.stderr) sys.exit(1)