Skip to content

Commit 23dfe7e

Browse files
committed
Add mini-swe-agent adapter and benchmark docs
1 parent 4e29cf5 commit 23dfe7e

22 files changed

Lines changed: 2214 additions & 54 deletions

bayesian_agent/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,31 @@
1616
from bayesian_agent.core.evidence import TrajectoryEvidence
1717
from bayesian_agent.core.policy import RewritePolicy
1818
from bayesian_agent.core.registry import BayesianSkillRegistry
19+
from bayesian_agent.harness import AgentHarness, HarnessTask, NativeBayesianAgentAdapter, ensure_harness
20+
from bayesian_agent.memory import CorticalMemory, HippocampusMemory, StateMemory, ThreeLayerMemory
1921

2022
__all__ = [
23+
"AgentHarness",
2124
"BayesianSkillRegistry",
2225
"BetaBernoulliState",
2326
"CATEGORICAL_BAYES",
27+
"CorticalMemory",
2428
"CategoricalBayesState",
2529
"DEFAULT_ALGORITHM",
30+
"HarnessTask",
31+
"HippocampusMemory",
2632
"NAIVE_BAYES_ALIAS",
2733
"NaiveBayesState",
34+
"NativeBayesianAgentAdapter",
2835
"RewriteDecision",
2936
"RewritePolicy",
3037
"SkillBelief",
3138
"SkillContextBuilder",
39+
"StateMemory",
3240
"SUPPORTED_ALGORITHMS",
41+
"ThreeLayerMemory",
3342
"TrajectoryEvidence",
43+
"ensure_harness",
3444
"features_from_event",
3545
"normalize_algorithm",
3646
]
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
"""Optional agent adapters."""
22

33
from bayesian_agent.adapters.base import AgentAdapter
4+
from bayesian_agent.adapters.bayesian_agent import NativeBayesianAgentAdapter
5+
from bayesian_agent.adapters.claude_code import ClaudeCodeAdapter
46
from bayesian_agent.adapters.generic_agent import GenericAgentAdapter
7+
from bayesian_agent.adapters.mini_swe_agent import MiniSWEAgentAdapter
58

6-
__all__ = ["AgentAdapter", "GenericAgentAdapter"]
9+
__all__ = [
10+
"AgentAdapter",
11+
"ClaudeCodeAdapter",
12+
"GenericAgentAdapter",
13+
"MiniSWEAgentAdapter",
14+
"NativeBayesianAgentAdapter",
15+
]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""First-party Bayesian-Agent harness adapter."""
2+
3+
from bayesian_agent.harness.native import NativeBayesianAgentAdapter
4+
5+
__all__ = ["NativeBayesianAgentAdapter"]
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
"""Claude Code CLI adapter."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
import os
7+
import signal
8+
import subprocess
9+
import time
10+
from dataclasses import dataclass
11+
from pathlib import Path
12+
from typing import Any, Mapping, Optional, Union
13+
14+
15+
@dataclass
16+
class ClaudeCodeAdapter:
17+
"""Run one task in a workspace using the local `claude` CLI."""
18+
19+
model: str = "deepseek-v4-flash"
20+
cli_path: str = "claude"
21+
permission_mode: str = "bypassPermissions"
22+
timeout_seconds: int = 900
23+
max_budget_usd: Optional[float] = None
24+
system_prompt: str = ""
25+
26+
def integration_note(self) -> str:
27+
return (
28+
"Claude Code integration is optional. Claude Code executes task prompts; "
29+
"Bayesian-Agent owns benchmark orchestration and result grading."
30+
)
31+
32+
def run(self, task: Mapping[str, Any], skill_context: str = "") -> Mapping[str, Any]:
33+
prompt = str(task["prompt"])
34+
if skill_context:
35+
prompt = f"{skill_context}\n{prompt}"
36+
return self.run_task(
37+
prompt=prompt,
38+
workspace=task["workspace"],
39+
max_turns=int(task.get("max_turns", 8) or 8),
40+
)
41+
42+
def build_task(self, *, prompt: str, workspace: Union[str, Path], max_turns: int = 8) -> Mapping[str, Any]:
43+
return {"prompt": prompt, "workspace": str(Path(workspace).resolve()), "max_turns": int(max_turns)}
44+
45+
def build_command(self) -> list[str]:
46+
command = [
47+
self.cli_path,
48+
"--print",
49+
"--output-format",
50+
"json",
51+
"--model",
52+
self.model,
53+
"--permission-mode",
54+
self.permission_mode,
55+
"--no-session-persistence",
56+
]
57+
if self.system_prompt:
58+
command.extend(["--append-system-prompt", self.system_prompt])
59+
if self.max_budget_usd is not None:
60+
command.extend(["--max-budget-usd", str(self.max_budget_usd)])
61+
return command
62+
63+
def run_task(self, *, prompt: str, workspace: Union[str, Path], max_turns: int = 8) -> Mapping[str, Any]:
64+
workspace_path = Path(workspace).resolve()
65+
workspace_path.mkdir(parents=True, exist_ok=True)
66+
command = self.build_command()
67+
started = time.time()
68+
raw_stdout = ""
69+
raw_stderr = ""
70+
try:
71+
process = subprocess.Popen(
72+
command,
73+
cwd=str(workspace_path),
74+
stdin=subprocess.PIPE,
75+
stdout=subprocess.PIPE,
76+
stderr=subprocess.PIPE,
77+
start_new_session=True,
78+
text=True,
79+
)
80+
raw_stdout, raw_stderr = process.communicate(input=prompt, timeout=self.timeout_seconds)
81+
elapsed = time.time() - started
82+
raw_stdout = raw_stdout or ""
83+
raw_stderr = raw_stderr or ""
84+
exit_code = process.returncode
85+
except subprocess.TimeoutExpired as exc:
86+
_terminate_process_group(process)
87+
try:
88+
timeout_stdout, timeout_stderr = process.communicate(timeout=5)
89+
except subprocess.TimeoutExpired:
90+
_kill_process_group(process)
91+
timeout_stdout, timeout_stderr = process.communicate()
92+
elapsed = time.time() - started
93+
raw_stdout = _decode_timeout_output(exc.stdout) or _decode_timeout_output(timeout_stdout)
94+
raw_stderr = _decode_timeout_output(exc.stderr) or _decode_timeout_output(timeout_stderr)
95+
exit_code = 124
96+
raw = {
97+
"type": "result",
98+
"is_error": True,
99+
"result": raw_stdout,
100+
"errors": [f"Claude Code timed out after {self.timeout_seconds} seconds."],
101+
}
102+
parsed = self.parse_result(raw)
103+
parsed["elapsed_seconds"] = elapsed
104+
parsed["exit_code"] = exit_code
105+
parsed["error"] = "; ".join(str(item) for item in parsed.get("errors") or [])[:2000]
106+
self._write_run_artifacts(workspace_path, command, raw_stdout, raw_stderr, parsed)
107+
return parsed
108+
(workspace_path / "claude_command.json").write_text(json.dumps(command, ensure_ascii=False, indent=2), encoding="utf-8")
109+
(workspace_path / "model_response_log.txt").write_text(raw_stdout, encoding="utf-8")
110+
if raw_stderr:
111+
(workspace_path / "claude_stderr.txt").write_text(raw_stderr, encoding="utf-8")
112+
try:
113+
raw = json.loads(raw_stdout)
114+
except json.JSONDecodeError:
115+
raw = {
116+
"type": "result",
117+
"is_error": True,
118+
"result": raw_stdout,
119+
"errors": [raw_stderr or "Claude Code returned non-JSON output."],
120+
}
121+
parsed = self.parse_result(raw)
122+
parsed["elapsed_seconds"] = elapsed
123+
parsed["exit_code"] = exit_code
124+
if exit_code != 0:
125+
errors = list(parsed.get("errors") or [])
126+
if raw_stderr:
127+
errors.append(raw_stderr[-2000:])
128+
parsed["errors"] = errors
129+
parsed["error"] = "; ".join(str(item) for item in errors)[:2000]
130+
(workspace_path / "transcript.txt").write_text(str(parsed.get("transcript") or ""), encoding="utf-8")
131+
return parsed
132+
133+
def load_run_from_workspace(self, workspace: Union[str, Path]) -> Optional[Mapping[str, Any]]:
134+
log_path = Path(workspace).resolve() / "model_response_log.txt"
135+
if not log_path.exists():
136+
return None
137+
try:
138+
raw = json.loads(log_path.read_text(encoding="utf-8"))
139+
except json.JSONDecodeError:
140+
return None
141+
parsed = dict(self.parse_result(raw))
142+
parsed["elapsed_seconds"] = 0.0
143+
parsed["exit_code"] = 0
144+
parsed["recovered_from_workspace"] = True
145+
return parsed
146+
147+
def parse_result(self, raw: Mapping[str, Any]) -> Mapping[str, Any]:
148+
model_usage = dict(raw.get("modelUsage") or {})
149+
input_tokens = 0
150+
output_tokens = 0
151+
cost = float(raw.get("total_cost_usd") or 0.0)
152+
for usage in model_usage.values():
153+
usage = dict(usage or {})
154+
input_tokens += int(usage.get("inputTokens") or 0)
155+
input_tokens += int(usage.get("cacheReadInputTokens") or 0)
156+
input_tokens += int(usage.get("cacheCreationInputTokens") or 0)
157+
output_tokens += int(usage.get("outputTokens") or 0)
158+
cost += float(usage.get("costUSD") or 0.0)
159+
if raw.get("total_cost_usd") is not None:
160+
cost = float(raw.get("total_cost_usd") or 0.0)
161+
transcript = str(raw.get("result") or raw.get("content") or raw.get("message") or "")
162+
errors = list(raw.get("errors") or [])
163+
return {
164+
"transcript": transcript,
165+
"exit_reason": str(raw.get("stop_reason") or raw.get("subtype") or ""),
166+
"input_tokens": input_tokens,
167+
"output_tokens": output_tokens,
168+
"total_tokens": input_tokens + output_tokens,
169+
"total_cost_usd": cost,
170+
"usage_events": [{"source": "claude_code", "model_usage": model_usage}],
171+
"model_usage": model_usage,
172+
"session_id": str(raw.get("session_id") or ""),
173+
"claude_uuid": str(raw.get("uuid") or ""),
174+
"is_error": bool(raw.get("is_error")),
175+
"errors": errors,
176+
}
177+
178+
def _write_run_artifacts(
179+
self,
180+
workspace_path: Path,
181+
command: list[str],
182+
raw_stdout: str,
183+
raw_stderr: str,
184+
parsed: Mapping[str, Any],
185+
) -> None:
186+
(workspace_path / "claude_command.json").write_text(json.dumps(command, ensure_ascii=False, indent=2), encoding="utf-8")
187+
(workspace_path / "model_response_log.txt").write_text(raw_stdout, encoding="utf-8")
188+
if raw_stderr:
189+
(workspace_path / "claude_stderr.txt").write_text(raw_stderr, encoding="utf-8")
190+
(workspace_path / "transcript.txt").write_text(str(parsed.get("transcript") or ""), encoding="utf-8")
191+
192+
193+
def _decode_timeout_output(value: Any) -> str:
194+
if value is None:
195+
return ""
196+
if isinstance(value, bytes):
197+
return value.decode("utf-8", errors="replace")
198+
return str(value)
199+
200+
201+
def _terminate_process_group(process: subprocess.Popen[str]) -> None:
202+
try:
203+
os.killpg(process.pid, signal.SIGTERM)
204+
except ProcessLookupError:
205+
return
206+
207+
208+
def _kill_process_group(process: subprocess.Popen[str]) -> None:
209+
try:
210+
os.killpg(process.pid, signal.SIGKILL)
211+
except ProcessLookupError:
212+
return

0 commit comments

Comments
 (0)