logic-engine / ace /runners /claude_code.py
ghostdrive1's picture
Upload folder using huggingface_hub
116524e verified
Raw
History Blame Contribute Delete
9.88 kB
"""ClaudeCode — Claude Code CLI runner with ACE learning."""
from __future__ import annotations
from collections.abc import Iterable, Sequence
from pathlib import Path
from typing import Any, Optional
from pydantic_ai.settings import ModelSettings
from pipeline import Pipeline
from pipeline.protocol import SampleResult, StepProtocol
from ..core.context import ACEStepContext, SkillbookView
from ..core.skillbook import Skillbook
from ..integrations import wrap_skillbook_context
from ..integrations.claude_code import ClaudeCodeExecuteStep, ClaudeCodeToTrace
from ..protocols import (
DeduplicationConfig,
DeduplicationManagerLike,
ReflectorLike,
SkillManagerLike,
)
from ..steps import learning_tail
from .base import ACERunner
class ClaudeCode(ACERunner):
"""Claude Code CLI with ACE learning pipeline.
INJECT skillbook → EXECUTE Claude Code → LEARN (Reflect → Tag → Update → Apply).
Two construction paths:
1. ``ClaudeCode.from_roles(reflector, skill_manager, working_dir=..., ...)``
— pre-built roles.
2. ``ClaudeCode.from_model(working_dir=..., ace_model="gpt-4o-mini", ...)``
— builds ACE roles from a model string.
Example::
runner = ClaudeCode.from_model(working_dir="./my_project")
results = runner.run([
"Add unit tests for utils.py",
"Refactor the auth module",
])
runner.save("code_expert.json")
"""
@classmethod
def build_steps(
cls,
*,
reflector: ReflectorLike,
skill_manager: SkillManagerLike,
skillbook: Skillbook | None = None,
skillbook_path: Optional[str] = None,
working_dir: Optional[str] = None,
timeout: int = 600,
model: Optional[str] = None,
allowed_tools: Optional[list[str]] = None,
dedup_config: Optional[DeduplicationConfig] = None,
dedup_manager: DeduplicationManagerLike | None = None,
dedup_interval: int = 10,
checkpoint_dir: str | Path | None = None,
checkpoint_interval: int = 10,
) -> list[StepProtocol]:
"""Return the steps that ``from_roles()`` would compose.
Use this to inspect, modify, or extend the pipeline before
constructing it yourself::
steps = ClaudeCode.build_steps(reflector=r, skill_manager=sm, ...)
steps.insert(2, MyCustomStep())
pipe = Pipeline(steps)
runner = ACERunner(pipeline=pipe, skillbook=skillbook)
Args:
reflector: Reflector role for analysing execution traces.
skill_manager: SkillManager role for update operations.
skillbook: Starting skillbook. Creates an empty one if ``None``.
skillbook_path: Path to load skillbook from.
working_dir: Directory where Claude Code executes.
timeout: Execution timeout in seconds.
model: Optional Claude model override.
allowed_tools: Optional list of allowed tools.
dedup_config: Deduplication configuration.
dedup_manager: Optional pre-built deduplication manager.
dedup_interval: Samples between deduplication runs.
checkpoint_dir: Directory for checkpoint files.
checkpoint_interval: Samples between checkpoint saves.
"""
# Resolve skillbook
if skillbook_path:
skillbook = Skillbook.load_from_file(skillbook_path)
elif skillbook is None:
skillbook = Skillbook()
# Resolve dedup manager
dm = dedup_manager
if dm is None and dedup_config is not None:
from ..deduplication import DeduplicationManager
dm = DeduplicationManager(dedup_config)
steps: list[StepProtocol[ACEStepContext]] = [
ClaudeCodeExecuteStep(
working_dir=working_dir,
timeout=timeout,
model=model,
allowed_tools=allowed_tools,
),
ClaudeCodeToTrace(),
*learning_tail(
reflector,
skill_manager,
skillbook,
dedup_manager=dm,
dedup_interval=dedup_interval,
checkpoint_dir=checkpoint_dir,
checkpoint_interval=checkpoint_interval,
),
]
return steps
@classmethod
def from_roles(
cls,
*,
reflector: ReflectorLike,
skill_manager: SkillManagerLike,
skillbook: Skillbook | None = None,
skillbook_path: Optional[str] = None,
working_dir: Optional[str] = None,
timeout: int = 600,
model: Optional[str] = None,
allowed_tools: Optional[list[str]] = None,
dedup_config: Optional[DeduplicationConfig] = None,
dedup_manager: DeduplicationManagerLike | None = None,
dedup_interval: int = 10,
checkpoint_dir: str | Path | None = None,
checkpoint_interval: int = 10,
) -> ClaudeCode:
"""Construct from pre-built role instances.
Args:
reflector: Reflector role for analysing execution traces.
skill_manager: SkillManager role for update operations.
skillbook: Starting skillbook. Creates an empty one if ``None``.
skillbook_path: Path to load skillbook from.
working_dir: Directory where Claude Code executes.
timeout: Execution timeout in seconds.
model: Optional Claude model override.
allowed_tools: Optional list of allowed tools.
dedup_config: Deduplication configuration.
dedup_manager: Optional pre-built deduplication manager.
dedup_interval: Samples between deduplication runs.
checkpoint_dir: Directory for checkpoint files.
checkpoint_interval: Samples between checkpoint saves.
"""
# Resolve skillbook (must match build_steps resolution)
if skillbook_path:
skillbook = Skillbook.load_from_file(skillbook_path)
elif skillbook is None:
skillbook = Skillbook()
steps = cls.build_steps(
reflector=reflector,
skill_manager=skill_manager,
skillbook=skillbook,
working_dir=working_dir,
timeout=timeout,
model=model,
allowed_tools=allowed_tools,
dedup_config=dedup_config,
dedup_manager=dedup_manager,
dedup_interval=dedup_interval,
checkpoint_dir=checkpoint_dir,
checkpoint_interval=checkpoint_interval,
)
return cls(pipeline=Pipeline(steps), skillbook=skillbook)
@classmethod
def from_model(
cls,
*,
working_dir: Optional[str] = None,
ace_model: str = "gpt-4o-mini",
ace_max_tokens: int = 2048,
ace_temperature: float = 0.0,
**kwargs: Any,
) -> ClaudeCode:
"""Build ACE roles from a model string.
Args:
working_dir: Directory where Claude Code executes.
ace_model: Model identifier for ACE roles.
ace_max_tokens: Max tokens for ACE LLM responses.
ace_temperature: Sampling temperature for ACE roles.
**kwargs: Forwarded to :meth:`from_roles`.
"""
from ..implementations import Reflector, SkillManager
model_settings = ModelSettings(
temperature=ace_temperature,
max_tokens=ace_max_tokens,
)
return cls.from_roles(
reflector=Reflector(ace_model, model_settings=model_settings),
skill_manager=SkillManager(ace_model, model_settings=model_settings),
working_dir=working_dir,
**kwargs,
)
def run(
self,
tasks: Sequence[str] | Iterable[str] | str,
epochs: int = 1,
*,
wait: bool = True,
) -> list[SampleResult]:
"""Run coding tasks with learning.
Args:
tasks: Single task string or list of task strings.
Must be a ``Sequence`` for ``epochs > 1``.
epochs: Number of passes over all tasks.
wait: If ``True``, block until background learning completes.
"""
if isinstance(tasks, str):
tasks = [tasks]
return self._run(tasks, epochs=epochs, wait=wait)
def _build_context( # type: ignore[override]
self,
task: str,
*,
epoch: int,
total_epochs: int,
index: int,
total: int | None,
global_sample_index: int,
**_: Any,
) -> ACEStepContext:
"""Place a raw task string on ``ctx.sample``."""
return ACEStepContext(
sample=task,
skillbook=SkillbookView(self.skillbook),
epoch=epoch,
total_epochs=total_epochs,
step_index=index,
total_steps=total,
global_sample_index=global_sample_index,
)
# ------------------------------------------------------------------
# Convenience lifecycle methods
# ------------------------------------------------------------------
def get_strategies(self) -> str:
"""Return formatted skillbook strategies for display."""
if not self.skillbook.skills():
return ""
return wrap_skillbook_context(self.skillbook)
# Backward-compat aliases
save_skillbook = ACERunner.save
load_skillbook = ACERunner.load
wait_for_learning = ACERunner.wait_for_background